diff --git a/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java new file mode 100644 index 0000000000..3669c834db --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java @@ -0,0 +1,488 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.redis.core.json.JsonPath; +import org.springframework.util.Assert; + +import java.util.List; + +/** + * JSON commands supported by Redis. + * + * @author Yordan Tsintsov + * @see RedisCommands + * @since 4.3 + */ +public interface RedisJsonCommands { + + /** + * Append the JSON values into the array at path after the last element in it. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param values must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @return a list where each element contains the number of elements added to the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRAPPEND + * @since 4.3 + */ + List<@Nullable Long> jsonArrAppend(byte[] key, JsonPath path, String... values); + + /** + * Search for the first occurrence of a JSON value in an array. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @return a list where each element contains the index of the first occurrence of the value in the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRINDEX + * @since 4.3 + */ + default List<@Nullable Long> jsonArrIndex(byte[] key, JsonPath path, String value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(value, "Value must not be null"); + + return jsonArrIndex(key, path, value, 0); + } + + /** + * Search for the first occurrence of a JSON value in an array. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @param start index to start searching from. + * @return a list where each element contains the index of the first occurrence of the value in the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRINDEX + * @since 4.3 + */ + default List<@Nullable Long> jsonArrIndex(byte[] key, JsonPath path, String value, long start) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(value, "Value must not be null"); + + return jsonArrIndex(key, path, value, start, 0); + } + + /** + * Search for the first occurrence of a JSON value in an array. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @param start index to start searching from. + * @param stop index to stop searching at. + * @return a list where each element contains the index of the first occurrence of the value in the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRINDEX + * @since 4.3 + */ + List<@Nullable Long> jsonArrIndex(byte[] key, JsonPath path, String value, long start, long stop); + + /** + * Insert the {@code values} into the array at {@code path} before {@code index}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param index to insert before. + * @param values must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @return a list where each element contains the size of the array after the insertion or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRINSERT + * @since 4.3 + */ + List<@Nullable Long> jsonArrInsert(byte[] key, JsonPath path, int index, String... values); + + /** + * Get the length of the array at the given path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the length of the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRLEN + * @since 4.3 + */ + List<@Nullable Long> jsonArrLen(byte[] key, JsonPath path); + + /** + * Pop and return the last value in the array at the specified path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the value at the end of the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRPOP + * @since 4.3 + */ + default List<@Nullable String> jsonArrPop(byte[] key, JsonPath path) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + + return jsonArrPop(key, path, -1); + } + + /** + * Pop and return the value at the given index in the array at the given path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param index to pop. + * @return a list where each element contains the value at the given index in the array or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRPOP + * @since 4.3 + */ + List<@Nullable String> jsonArrPop(byte[] key, JsonPath path, int index); + + /** + * Trim the array at the given path to the given range. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param start index to start trimming from. + * @param stop index to stop trimming at. + * @return a list where each element contains the length of the array after the trim or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.ARRTRIM + * @since 4.3 + */ + List<@Nullable Long> jsonArrTrim(byte[] key, JsonPath path, int start, int stop); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the given key. + * + * @param key must not be {@literal null}. + * @return the number of paths cleared. + * @see Redis Documentation: JSON.CLEAR + * @since 4.3 + */ + default Long jsonClear(byte[] key) { + + Assert.notNull(key, "Key must not be null"); + + return jsonClear(key, JsonPath.root()); + } + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return the number of paths cleared. + * @see Redis Documentation: JSON.CLEAR + * @since 4.3 + */ + Long jsonClear(byte[] key, JsonPath path); + + /** + * Delete the JSON value at the given key. + * + * @param key must not be {@literal null}. + * @return the number of paths deleted. + * @see Redis Documentation: JSON.DEL + * @since 4.3 + */ + default Long jsonDel(byte[] key) { + + Assert.notNull(key, "Key must not be null"); + + return jsonDel(key, JsonPath.root()); + } + + /** + * Delete the JSON value at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return the number of paths deleted. + * @see Redis Documentation: JSON.DEL + * @since 4.3 + */ + Long jsonDel(byte[] key, JsonPath path); + + /** + * Get the JSON values at the given key. + * + * @param key must not be {@literal null}. + * @return list where each element is a JSON values or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.GET + * @since 4.3 + */ + default @Nullable String jsonGet(byte[] key) { + + Assert.notNull(key, "Key must not be null"); + + List<@Nullable String> result = jsonGet(key, JsonPath.root()); + + return result.isEmpty() ? null : result.getFirst(); + } + + /** + * Get the JSON values at the given key and paths. + * + * @param key must not be {@literal null}. + * @param paths must not be {@literal null}. + * @return list where each element is a JSON values or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.GET + * @since 4.3 + */ + List<@Nullable String> jsonGet(byte[] key, JsonPath... paths); + + /** + * Merge the JSON value at the given {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @return {@literal true} if the key was merged, {@literal false} otherwise. + * @see Redis Documentation: JSON.MERGE + * @since 4.3 + */ + default Boolean jsonMerge(byte[] key, String value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return jsonMerge(key, JsonPath.root(), value); + } + + /** + * Merge the JSON value at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. {@literal null} values should be represented as JSON "null" values. + * @return {@literal true} if the key was merged, {@literal false} otherwise. + * @see Redis Documentation: JSON.MERGE + * @since 4.3 + */ + Boolean jsonMerge(byte[] key, JsonPath path, String value); + + /** + * Get the JSON values at the given keys. + * + * @param keys must not be {@literal null}. + * @return list of JSON values or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.MGET + * @since 4.3 + */ + default List<@Nullable String> jsonMGet(byte[]... keys) { + + Assert.notEmpty(keys, "Keys must not be empty"); + Assert.noNullElements(keys, "Keys must not be null"); + + return jsonMGet(JsonPath.root(), keys); + } + + /** + * Get the JSON values at the given keys and paths. + * + * @param path must not be {@literal null}. + * @param keys must not be {@literal null}. + * @return list of JSON values or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.MGET + * @since 4.3 + */ + List<@Nullable String> jsonMGet(JsonPath path, byte[]... keys); + + /** + * Set the JSON values at the given keys and paths. + * + * @param args must not be {@literal null}. + * @return {@literal true} if the keys were set, {@literal false} otherwise. + * @see Redis Documentation: JSON.MSET + * @since 4.3 + */ + Boolean jsonMSet(List args); + + /** + * Increment the number value at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param number must not be {@literal null}. + * @return a list where each element is the new value or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.NUMINCRBY + * @since 4.3 + */ + List<@Nullable Number> jsonNumIncrBy(byte[] key, JsonPath path, Number number); + + /** + * Set the JSON value at the given key. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@literal true} if the key was set, {@literal false} otherwise. + * @see Redis Documentation: JSON.SET + * @since 4.3 + */ + default Boolean jsonSet(byte[] key, String value) { + + Assert.notNull(key, "Key must not be null"); + + return jsonSet(key, JsonPath.root(), value, JsonSetOption.upsert()); + } + + /** + * Set the JSON value at the given key. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. + * @param option must not be {@literal null}. + * @return {@literal true} if the key was set, {@literal false} otherwise. + * @see Redis Documentation: JSON.SET + * @since 4.3 + */ + Boolean jsonSet(byte[] key, JsonPath path, String value, JsonSetOption option); + + /** + * Append the string JSON value into the string at path after the last character. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. + * @return a list where each element is the new string length or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.STRAPPEND + * @since 4.3 + */ + List<@Nullable Long> jsonStrAppend(byte[] key, JsonPath path, String value); + + /** + * Get the string length at the given path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element is the string length or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.STRLEN + * @since 4.3 + */ + List<@Nullable Long> jsonStrLen(byte[] key, JsonPath path); + + /** + * Toggle boolean values at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element is the new value or {@literal null} if path does not exist. + * @see Redis Documentation: JSON.TOGGLE + * @since 4.3 + */ + List<@Nullable Boolean> jsonToggle(byte[] key, JsonPath path); + + /** + * Get the JSON type at the given key. + * + * @param key must not be {@literal null}. + * @return a list where each element is the type at the given path. + * @see Redis Documentation: JSON.TYPE + * @since 4.3 + */ + default List jsonType(byte[] key) { + + Assert.notNull(key, "Key must not be null"); + + return jsonType(key, JsonPath.root()); + } + + /** + * Get the JSON type at the given key and path. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element is the type at the given path. + * @see Redis Documentation: JSON.TYPE + * @since 4.3 + */ + List jsonType(byte[] key, JsonPath path); + + /** + * {@code JSON.SET} command arguments for {@code NX}, {@code XX}. + */ + enum JsonSetOption { + + /** + * Do not set any additional command argument. + */ + UPSERT, + + /** + * {@code NX} + */ + IF_PATH_NOT_EXISTS, + + /** + * {@code XX} + */ + IF_PATH_EXISTS; + + /** + * Do not set any additional command argument. + * + * @return {@link JsonSetOption#UPSERT} + */ + public static JsonSetOption upsert() { + return UPSERT; + } + + /** + * {@code NX} + * + * @return {@link JsonSetOption#IF_PATH_NOT_EXISTS} + */ + public static JsonSetOption ifPathNotExists() { + return IF_PATH_NOT_EXISTS; + } + + /** + * {@code XX} + * + * @return {@link JsonSetOption#IF_PATH_EXISTS} + */ + public static JsonSetOption ifPathExists() { + return IF_PATH_EXISTS; + } + + } + + /** + * Arguments for {@code JSON.MSET} command. + * + * @param key the key, must not be {@literal null}. + * @param path the JSON path, must not be {@literal null}. + * @param value the value to set. + * @since 4.3 + */ + record JsonMSetArgs(byte[] key, JsonPath path, String value) { + + public JsonMSetArgs { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(value, "Value must not be null"); + } + + public JsonMSetArgs(byte[] key, String value) { + this(key, JsonPath.root(), value); + } + + } + + enum JsonType { + + NULL, STRING, NUMBER, BOOLEAN, OBJECT, ARRAY; + + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/JsonOperations.java b/src/main/java/org/springframework/data/redis/core/JsonOperations.java new file mode 100644 index 0000000000..7cf11f7ba1 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/JsonOperations.java @@ -0,0 +1,608 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.util.Collection; +import java.util.List; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.core.json.JsonPath; +import org.springframework.util.Assert; + +/** + * Redis JSON specific operations, working on JSON documents stored in Redis. + *

+ * This interface provides high-level operations for manipulating JSON data structures + * using the Redis JSON module. Operations include reading and writing JSON values, + * array manipulation, string operations, and numeric operations at specific JSON paths. + *

+ * JSON paths follow the JSONPath syntax, where {@code $} represents the root element. + * Use {@link JsonPath} to construct paths programmatically. + * + * @author Yordan Tsintsov + * @see Redis JSON Documentation + * @since 4.3 + */ +@NullUnmarked +public interface JsonOperations { + + /** + * Append {@code values} to the JSON array at {@code path} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param values can be {@literal null}. + * @return a list where each element contains the new array length at matching paths, + * or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRAPPEND + */ + List<@Nullable Long> arrayAppend(@NonNull K key, @NonNull JsonPath path, Object... values); + + /** + * Search for the first occurrence of {@code value} in the JSON array at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @return a list where each element contains the index of the first occurrence of the value, + * {@literal -1} if not found, or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRINDEX + */ + default List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + + return arrayIndex(key, path, value, Range.unbounded()); + } + + /** + * Search for the first occurrence of {@code value} in the JSON array at {@code path}, + * within the {@code range}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @param range must not be {@literal null}. + * @return a list where each element contains the index of the first occurrence of the value, + * {@literal -1} if not found, or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRINDEX + */ + List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value, @NonNull Range<@NonNull Long> range); + + /** + * Insert {@code values} into the JSON array at {@code path} before the element at {@code index}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param index the position to insert before. Negative values count from the end of the array. + * @param values can be {@literal null}. + * @return a list where each element contains the new array length at matching paths, + * or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRINSERT + */ + List<@Nullable Long> arrayInsert(@NonNull K key, @NonNull JsonPath path, int index, Object... values); + + /** + * Get the length of the JSON array at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the array length at matching paths, + * or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRLEN + */ + List<@Nullable Long> arrayLength(@NonNull K key, @NonNull JsonPath path); + + /** + * Remove and return the last element from the JSON array at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @return a list where each element contains the popped value at matching paths, + * or {@literal null} if the path does not exist, is not an array, or the array is empty. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRPOP + */ + default List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull Class clazz) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(clazz, "Class must not be null"); + + return arrayPop(key, path, clazz, -1); + } + + /** + * Remove and return the element at {@code index} from the JSON array at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @param index the position to pop from. Negative values count from the end of the array. + * @return a list where each element contains the popped value at matching paths, + * or {@literal null} if the path does not exist, is not an array, or index is out of bounds. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRPOP + */ + List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull Class clazz, int index); + + /** + * Remove and return the last element from the JSON array at {@code path}. Use this variant when the target value is a nested object. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @return a list where each element contains the popped value at matching paths, + * or {@literal null} if the path does not exist, is not an array, or index is out of bounds. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRPOP + */ + default List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull ParameterizedTypeReference<@NonNull T> typeRef) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(typeRef, "TypeReference must not be null"); + + return arrayPop(key, path, typeRef, -1); + } + + /** + * Remove and return the element at {@code index} from the JSON array at {@code path}. Use this variant when the target value is a nested object. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @param index the position to pop from. Negative values count from the end of the array. + * @return a list where each element contains the popped value at matching paths, + * or {@literal null} if the path does not exist, is not an array, or index is out of bounds. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRPOP + */ + List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull ParameterizedTypeReference<@NonNull T> typeRef, int index); + + /** + * Trim the JSON array at {@code path} to contain only elements within the range [{@code start}, {@code stop}]. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param start the start index (inclusive). Negative values count from the end of the array. + * @param stop the stop index (inclusive). Negative values count from the end of the array. + * @return a list where each element contains the new array length at matching paths, + * or {@literal null} if the path does not exist or is not an array. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.ARRTRIM + */ + List<@Nullable Long> arrayTrim(@NonNull K key, @NonNull JsonPath path, int start, int stop); + + /** + * Clear container values (arrays/objects) and set numeric values to {@literal 0} at the root path + * of the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return the number of values cleared. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.CLEAR + */ + default Long clear(@NonNull K key) { + + Assert.notNull(key, "Key must not be null"); + + return clear(key, JsonPath.root()); + } + + /** + * Clear container values (arrays/objects) and set numeric values to {@literal 0} at {@code path} + * in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return the number of values cleared. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.CLEAR + */ + Long clear(@NonNull K key, @NonNull JsonPath path); + + /** + * Delete the entire JSON document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return the number of paths deleted (0 or 1). {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.DEL + */ + default Long delete(@NonNull K key) { + + Assert.notNull(key, "Key must not be null"); + + return delete(key, JsonPath.root()); + } + + /** + * Delete the JSON value at {@code path} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return the number of paths deleted. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.DEL + */ + Long delete(@NonNull K key, @NonNull JsonPath path); + + /** + * Get the entire JSON document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @return the deserialized JSON document, or {@literal null} if the key does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.GET + */ + default @Nullable T get(@NonNull K key, @NonNull Class clazz) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(clazz, "Class must not be null"); + + List<@Nullable T> result = get(key, clazz, JsonPath.root()); + + return result.isEmpty() ? null : result.getFirst(); + } + + /** + * Get the JSON values at multiple {@code paths} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @param paths must not be {@literal null}. + * @return a list where each element contains the value at matching paths, + * or {@literal null} if the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.GET + */ + List<@Nullable T> get(@NonNull K key, @NonNull Class clazz, @NonNull JsonPath @NonNull... paths); + + /** + * Get the entire JSON document stored at {@code key}. Use this variant when the target value is a nested object. + * + * @param key must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @return the deserialized JSON document, or {@literal null} if the key does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.GET + */ + default @Nullable T get(@NonNull K key, @NonNull ParameterizedTypeReference<@NonNull T> typeRef) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(typeRef, "Type reference must not be null"); + + List<@Nullable T> result = get(key, typeRef, JsonPath.root()); + + return result.isEmpty() ? null : result.getFirst(); + } + + /** + * Get the JSON values at multiple {@code paths} in the document stored at {@code key}. Use this variant when the target value is a nested object. + * + * @param key must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @param paths must not be {@literal null}. + * @return a list where each element contains the value at matching paths, + * or {@literal null} if the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.GET + */ + List<@Nullable T> get(@NonNull K key, @NonNull ParameterizedTypeReference<@NonNull T> typeRef, @NonNull JsonPath @NonNull... paths); + + /** + * Increment the numeric value at {@code path} by {@code number}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param number must not be {@literal null}. + * @return a list where each element contains the new value at matching paths, + * or {@literal null} if the path does not exist or is not a number. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.NUMINCRBY + */ + List<@Nullable Number> increment(@NonNull K key, @NonNull JsonPath path, @NonNull Number number); + + /** + * Merge {@code value} into the root of the JSON document stored at {@code key}. + *

+ * Merging follows RFC 7396 semantics: existing object keys are updated or added, + * and values set to {@literal null} are deleted. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@literal true} if the merge was successful, {@literal false} otherwise. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MERGE + */ + default Boolean merge(@NonNull K key, Object value) { + + Assert.notNull(key, "Key must not be null"); + + return merge(key, JsonPath.root(), value); + } + + /** + * Merge {@code value} into the JSON document at {@code path} stored at {@code key}. + *

+ * Merging follows RFC 7396 semantics: existing object keys are updated or added, + * and values set to {@literal null} are deleted. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the merge was successful, {@literal false} otherwise. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MERGE + */ + Boolean merge(@NonNull K key, @NonNull JsonPath path, Object value); + + /** + * Get the JSON documents stored at multiple {@code keys}. + * + * @param keys must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @return a list of values at the root path for each key, with {@literal null} for keys that do not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MGET + */ + default List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull Class clazz) { + + Assert.notEmpty(keys, "Keys must not be null or empty"); + Assert.notNull(clazz, "Class must not be null"); + + return multiGet(keys, clazz, JsonPath.root()); + } + + /** + * Get the JSON values at {@code path} from documents stored at multiple {@code keys}. + * + * @param keys must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list of values at the path for each key, with {@literal null} for keys where the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MGET + */ + List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull Class clazz, @NonNull JsonPath path); + + /** + * Get the JSON documents stored at multiple {@code keys}. Use this variant when the target value is a nested object. + * + * @param keys must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @return a list of values at the root path for each key, with {@literal null} for keys that do not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MGET + */ + default List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull ParameterizedTypeReference<@NonNull T> typeRef) { + + Assert.notEmpty(keys, "Keys must not be null or empty"); + Assert.notNull(typeRef, "TypeReference must not be null"); + + return multiGet(keys, typeRef, JsonPath.root()); + } + + /** + * Get the JSON values at {@code path} from documents stored at multiple {@code keys}. Use this variant when the target value is a nested object. + * + * @param keys must not be {@literal null}. + * @param typeRef must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list of values at the path for each key, with {@literal null} for keys where the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MGET + */ + List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull ParameterizedTypeReference<@NonNull T> typeRef, @NonNull JsonPath path); + + /** + * Set JSON values at the specified keys and paths atomically. + * + * @param args must not be {@literal null}. + * @return {@literal true} if all values were set successfully, {@literal false} otherwise. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.MSET + */ + Boolean multiSet(@NonNull List> args); + + /** + * Set the JSON {@code value} at the root path of the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set successfully, {@literal false} otherwise. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + default Boolean set(@NonNull K key, Object value) { + + Assert.notNull(key, "Key must not be null"); + + return set(key, JsonPath.root(), value); + } + + /** + * Set the JSON {@code value} at {@code path} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set successfully, {@literal false} otherwise. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + Boolean set(@NonNull K key, @NonNull JsonPath path, Object value); + + /** + * Set the JSON {@code value} at the root path only if the key does not already exist. + * + * @param key must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set, {@literal false} if the key already exists. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + Boolean setIfAbsent(@NonNull K key, Object value); + + /** + * Set the JSON {@code value} at the root path only if the key already exists. + * + * @param key must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set, {@literal false} if the key does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + Boolean setIfPresent(@NonNull K key, Object value); + + /** + * Set the JSON {@code value} at {@code path} only if the path does not already exist. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set, {@literal false} if the path already exists. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + Boolean setIfPathAbsent(@NonNull K key, @NonNull JsonPath path, Object value); + + /** + * Set the JSON {@code value} at {@code path} only if the path already exists. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @return {@literal true} if the value was set, {@literal false} if the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.SET + */ + Boolean setIfPathPresent(@NonNull K key, @NonNull JsonPath path, Object value); + + /** + * Append {@code value} to the JSON string at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value must not be {@literal null}. + * @return a list where each element contains the new string length at matching paths, + * or {@literal null} if the path does not exist or is not a string. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.STRAPPEND + */ + List<@Nullable Long> stringAppend(@NonNull K key, @NonNull JsonPath path, @NonNull String value); + + /** + * Get the length of the JSON string at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the string length at matching paths, + * or {@literal null} if the path does not exist or is not a string. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.STRLEN + */ + List<@Nullable Long> stringLength(@NonNull K key, @NonNull JsonPath path); + + /** + * Toggle the JSON boolean value at {@code path}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the new boolean value at matching paths, + * or {@literal null} if the path does not exist or is not a boolean. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.TOGGLE + */ + List<@Nullable Boolean> toggle(@NonNull K key, @NonNull JsonPath path); + + /** + * Get the JSON type at the root path of the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return a list containing the type at the root path. Returns an empty list if the key does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.TYPE + */ + default List> type(@NonNull K key) { + + Assert.notNull(key, "Key must not be null"); + + return type(key, JsonPath.root()); + } + + /** + * Get the JSON type at {@code path} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @return a list where each element contains the type at matching paths. + * Returns an empty list if the path does not exist. + * {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: JSON.TYPE + */ + List> type(@NonNull K key, @NonNull JsonPath path); + + /** + * @return the underlying {@link RedisOperations} used to execute commands. + */ + @NonNull + RedisOperations getOperations(); + + /** + * Arguments for {@link #multiSet(List)} operation. + * + * @param key the key, must not be {@literal null}. + * @param path the JSON path, must not be {@literal null}. + * @param value can be {@literal null}. + */ + record JsonMultiSetArgs(@NonNull K key, @NonNull JsonPath path, Object value) { + + /** + * Creates a new {@link JsonMultiSetArgs} with validation. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + */ + public JsonMultiSetArgs { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + } + + /** + * Creates a new {@link JsonMultiSetArgs} for the root path. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + */ + public JsonMultiSetArgs(K key, Object value) { + this(key, JsonPath.root(), value); + } + + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/json/JsonPath.java b/src/main/java/org/springframework/data/redis/core/json/JsonPath.java new file mode 100644 index 0000000000..9c7d0f5405 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/json/JsonPath.java @@ -0,0 +1,97 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.json; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; + +import java.util.Objects; + +/** + * Immutable value object representing a JSONPath expression for use with Redis JSON operations. + * + * @author Yordan Tsintsov + * @see Redis JSONPath Documentation + * @since 4.3 + */ +@NullMarked +public final class JsonPath { + + private static final String ROOT_PATH = "$"; + private static final JsonPath ROOT = new JsonPath(ROOT_PATH); + + private final String path; + + private JsonPath(String path) { + this.path = path; + } + + /** + * Returns the root path {@code $}. + * + * @return the root {@link JsonPath} + */ + public static JsonPath root() { + return ROOT; + } + + /** + * Creates a {@link JsonPath} from a raw path string. + * + * @param path the path expression, must not be empty + * @return a new {@link JsonPath} + */ + public static JsonPath of(String path) { + + Assert.hasText(path, "Path must not be empty"); + + return new JsonPath(path); + } + + /** + * Returns the path as a string. + * + * @return the path expression + */ + public String getPath() { + return path; + } + + @Override + public boolean equals(@Nullable Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JsonPath jsonPath = (JsonPath) o; + return Objects.equals(path, jsonPath.path); + } + + @Override + public int hashCode() { + return Objects.hashCode(path); + } + + @Override + public String toString() { + return path; + } + +}