From 57b6d30d10264900be15116c1a4c65a514180b85 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Wed, 11 Mar 2026 15:35:30 +0200 Subject: [PATCH 1/4] Implemented connection and template layer interfaces for Redis JSON operations. Furthermore, added a JsonPath class for easier path creating for JSON methods in the template layer. These are draft classes and their purpose is to get feedback and an initial feeling of how the JSON API should look like. Signed-off-by: Yordan Tsintsov --- .../redis/connection/RedisJsonCommands.java | 469 ++++++++++++++ .../data/redis/core/JsonOperations.java | 577 ++++++++++++++++++ .../data/redis/core/json/JsonPath.java | 194 ++++++ 3 files changed, 1240 insertions(+) create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java create mode 100644 src/main/java/org/springframework/data/redis/core/JsonOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/json/JsonPath.java 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..3c40eb4a26 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java @@ -0,0 +1,469 @@ +/* + * 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.util.Assert; + +import java.util.List; + +/** + * JSON commands supported by Redis. + * + * @author Yordan Tsintsov + * @see RedisCommands + * @since 4.3 + */ +public interface RedisJsonCommands { + + String ROOT_PATH = "$"; + + /** + * 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, String 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 + */ + List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value); + + /** + * 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 + */ + List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value, long start); + + /** + * 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, String 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, String 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, String 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 + */ + List<@Nullable String> jsonArrPop(byte[] key, String path); + + /** + * 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, String 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, String 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, ROOT_PATH); + } + + /** + * 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, String 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, ROOT_PATH); + } + + /** + * 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, String 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, ROOT_PATH); + return result.isEmpty() ? null : result.getFirst(); } + + /** + * Get the JSON values at the given key and paths. + * + * @param key must not be {@literal null}. + * @param path 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, String path); + + /** + * 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, String... 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 + */ + Boolean jsonMerge(byte[] key, String 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, String 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(ROOT_PATH, 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(String 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, String 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, ROOT_PATH, 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, String 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, String 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, String 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, String 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) { + return jsonType(key, ROOT_PATH); + } + + /** + * 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, String 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, String 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, ROOT_PATH, 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..0108165c89 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/JsonOperations.java @@ -0,0 +1,577 @@ +/* + * 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.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 JsonPath + * @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 + */ + List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value); + + /** + * Search for the first occurrence of {@code value} in the JSON array at {@code path}, + * starting from index {@code start}. + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @param start the index to start searching from (inclusive). + * @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, long start); + + /** + * Search for the first occurrence of {@code value} in the JSON array at {@code path}, + * within the range [{@code start}, {@code stop}). + * + * @param key must not be {@literal null}. + * @param path must not be {@literal null}. + * @param value can be {@literal null}. + * @param start the index to start searching from (inclusive). + * @param stop the index to stop searching at (exclusive). Use {@literal 0} to search to the end. + * @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, long start, long stop); + + /** + * 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 + */ + List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull Class clazz); + + /** + * 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 + */ + List<@Nullable T> arrayPop(@NonNull K key, @NonNull JsonPath path, @NonNull ParameterizedTypeReference<@NonNull T> typeRef); + + /** + * 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 + */ + Long clear(@NonNull K key); + + /** + * 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 + */ + Long delete(@NonNull K key); + + /** + * 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 + */ + @Nullable T get(@NonNull K key, @NonNull Class clazz); + + /** + * Get the JSON value at {@code path} in the document stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param clazz must not be {@literal null}. + * @param path 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 path); + + /** + * 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 + */ + @Nullable T get(@NonNull K key, @NonNull ParameterizedTypeReference<@NonNull T> typeRef); + + /** + * Get the JSON value at {@code path} 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 path 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 path); + + /** + * 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 + */ + Boolean merge(@NonNull K key, Object 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 + */ + List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull Class clazz); + + /** + * 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 + */ + List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull ParameterizedTypeReference<@NonNull T> typeRef); + + /** + * 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 + */ + Boolean set(@NonNull K key, Object 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 + */ + List> type(@NonNull K key); + + /** + * 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..48876860da --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/json/JsonPath.java @@ -0,0 +1,194 @@ +/* + * 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.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * JSON path. + * + * @author Yordan Tsintsov + * @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; + } + + public static JsonPath root() { + return ROOT; + } + + public static JsonPath of(String path) { + + Assert.hasText(path, "Path must not be empty"); + + return new JsonPath(path); + } + + public JsonPath child(String child) { + + Assert.hasText(child, "Child must not be empty"); + + return new JsonPath(path + "." + child); + } + + public JsonPath index(int index) { + return new JsonPath(path + "[" + index + "]"); + } + + public JsonPath first() { + return new JsonPath(path + "[0]"); + } + + public JsonPath last() { + return new JsonPath(path + "[-1]"); + } + + public JsonPath recursive() { + return new JsonPath(path + ".."); + } + + public JsonPath wildcard() { + return new JsonPath(path + ".*"); + } + + public JsonPath allElements() { + return new JsonPath(path + "[*]"); + } + + public JsonPath slice(int start) { + return new JsonPath(path + "[" + start + ":]"); + } + + public JsonPath slice(int start, int end) { + return new JsonPath(path + "[" + start + ":" + end + "]"); + } + + public JsonPath slice(int start, int end, int step) { + + Assert.isTrue(step != 0, "Step must not be zero"); + + return new JsonPath(path + "[" + start + ":" + end + ":" + step + "]"); + } + + public JsonPath indices(int... indices) { + + Assert.isTrue(indices.length > 0, "Indices must not be empty"); + + String joined = Arrays.stream(indices) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",")); + + return new JsonPath(path + "[" + joined + "]"); + } + + public JsonPath children(String... names) { + + Assert.notEmpty(names, "Names must not be empty"); + + String joined = Arrays.stream(names) + .map(n -> "'" + n + "'") + .collect(Collectors.joining(",")); + + return new JsonPath(path + "[" + joined + "]"); + } + + public String getPath() { + return path; + } + + public JsonPath filter(FilterExpression filter) { + return new JsonPath(path + filter); + } + + @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; + } + + public static final class FilterExpression { + + private final String expression; + + private FilterExpression(String expression) { + this.expression = expression; + } + + public static FilterExpression of(String expression) { + + Assert.hasText(expression, "Expression must not be empty"); + + return new FilterExpression(expression); + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FilterExpression that = (FilterExpression) o; + return Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() { + return Objects.hashCode(expression); + } + + @Override + public String toString() { + return "[?(" + expression + ")]"; + } + + } + +} From 703fb5a759a97ca1f014d86c0398118c83315bf8 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Thu, 12 Mar 2026 15:21:13 +0200 Subject: [PATCH 2/4] Reduced API surface by removing unnecessary overloads. Added default implementations for some of the overloaded classes. Signed-off-by: Yordan Tsintsov --- .../redis/connection/RedisJsonCommands.java | 50 +++++-- .../data/redis/core/JsonOperations.java | 141 +++++++++++------- .../data/redis/core/json/JsonArrayRange.java | 123 +++++++++++++++ 3 files changed, 244 insertions(+), 70 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java diff --git a/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java index 3c40eb4a26..0c1150783b 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java @@ -53,7 +53,14 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINDEX * @since 4.3 */ - List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value); + default List<@Nullable Long> jsonArrIndex(byte[] key, String 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. @@ -66,7 +73,14 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINDEX * @since 4.3 */ - List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value, long start); + default List<@Nullable Long> jsonArrIndex(byte[] key, String 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. @@ -115,7 +129,13 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRPOP * @since 4.3 */ - List<@Nullable String> jsonArrPop(byte[] key, String path); + default List<@Nullable String> jsonArrPop(byte[] key, String 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. @@ -207,18 +227,9 @@ default Long jsonDel(byte[] key) { Assert.notNull(key, "Key must not be null"); List<@Nullable String> result = jsonGet(key, ROOT_PATH); - return result.isEmpty() ? null : result.getFirst(); } - /** - * Get the JSON values at the given key and paths. - * - * @param key must not be {@literal null}. - * @param path 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, String path); + return result.isEmpty() ? null : result.getFirst(); + } /** * Get the JSON values at the given key and paths. @@ -240,7 +251,13 @@ default Long jsonDel(byte[] key) { * @see Redis Documentation: JSON.MERGE * @since 4.3 */ - Boolean jsonMerge(byte[] key, String value); + 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, ROOT_PATH, value); + } /** * Merge the JSON value at the given key and path. @@ -375,6 +392,9 @@ default Boolean jsonSet(byte[] key, String value) { * @since 4.3 */ default List jsonType(byte[] key) { + + Assert.notNull(key, "Key must not be null"); + return jsonType(key, ROOT_PATH); } diff --git a/src/main/java/org/springframework/data/redis/core/JsonOperations.java b/src/main/java/org/springframework/data/redis/core/JsonOperations.java index 0108165c89..ab2b0766ba 100644 --- a/src/main/java/org/springframework/data/redis/core/JsonOperations.java +++ b/src/main/java/org/springframework/data/redis/core/JsonOperations.java @@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.redis.core.json.JsonArrayRange; import org.springframework.data.redis.core.json.JsonPath; import org.springframework.util.Assert; @@ -37,7 +38,6 @@ * Use {@link JsonPath} to construct paths programmatically. * * @author Yordan Tsintsov - * @see JsonPath * @see Redis JSON Documentation * @since 4.3 */ @@ -68,38 +68,28 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.ARRINDEX */ - List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value); + default List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value) { - /** - * Search for the first occurrence of {@code value} in the JSON array at {@code path}, - * starting from index {@code start}. - * - * @param key must not be {@literal null}. - * @param path must not be {@literal null}. - * @param value can be {@literal null}. - * @param start the index to start searching from (inclusive). - * @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, long start); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(path, "Path must not be null"); + + return arrayIndex(key, path, value, JsonArrayRange.unbounded()); + } /** * Search for the first occurrence of {@code value} in the JSON array at {@code path}, - * within the range [{@code start}, {@code stop}). + * 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 start the index to start searching from (inclusive). - * @param stop the index to stop searching at (exclusive). Use {@literal 0} to search to the end. + * @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, long start, long stop); + List<@Nullable Long> arrayIndex(@NonNull K key, @NonNull JsonPath path, Object value, @NonNull JsonArrayRange range); /** * Insert {@code values} into the JSON array at {@code path} before the element at {@code index}. @@ -138,7 +128,14 @@ public interface JsonOperations { * {@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); + 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}. @@ -165,7 +162,14 @@ public interface JsonOperations { * {@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); + 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. @@ -203,7 +207,12 @@ public interface JsonOperations { * @return the number of values cleared. {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.CLEAR */ - Long clear(@NonNull K key); + 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} @@ -223,7 +232,12 @@ public interface JsonOperations { * @return the number of paths deleted (0 or 1). {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.DEL */ - Long delete(@NonNull K key); + 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}. @@ -244,20 +258,15 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.GET */ - @Nullable T get(@NonNull K key, @NonNull Class clazz); + default @Nullable T get(@NonNull K key, @NonNull Class clazz) { - /** - * Get the JSON value at {@code path} in the document stored at {@code key}. - * - * @param key must not be {@literal null}. - * @param clazz must not be {@literal null}. - * @param path 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 path); + 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}. @@ -281,20 +290,15 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.GET */ - @Nullable T get(@NonNull K key, @NonNull ParameterizedTypeReference<@NonNull T> typeRef); + default @Nullable T get(@NonNull K key, @NonNull ParameterizedTypeReference<@NonNull T> typeRef) { - /** - * Get the JSON value at {@code path} 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 path 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 path); + 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. @@ -334,7 +338,12 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.MERGE */ - Boolean merge(@NonNull K key, Object value); + 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}. @@ -360,7 +369,13 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.MGET */ - List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull Class clazz); + 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}. @@ -383,7 +398,13 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.MGET */ - List<@Nullable T> multiGet(@NonNull Collection keys, @NonNull ParameterizedTypeReference<@NonNull T> typeRef); + 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. @@ -416,7 +437,12 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.SET */ - Boolean set(@NonNull K key, Object value); + 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}. @@ -521,7 +547,12 @@ public interface JsonOperations { * {@literal null} when used in pipeline / transaction. * @see Redis Documentation: JSON.TYPE */ - List> type(@NonNull K key); + 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}. diff --git a/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java b/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java new file mode 100644 index 0000000000..ada55d9ce2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java @@ -0,0 +1,123 @@ +/* + * 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; + +/** + * Value object representing a range of array elements. + * + * @author Yordan Tsintsov + * @since 4.3 + * @see Redis JSON.ARRINDEX + */ +@NullMarked +public final class JsonArrayRange { + + private static final JsonArrayRange UNBOUNDED = new JsonArrayRange(0, 0); + + private final long start; + private final long stop; + + private JsonArrayRange(long start, long stop) { + this.start = start; + this.stop = stop; + } + + /** + * Create a new {@link JsonArrayRange} with the given start and stop indices. + * + * @param start the start index (inclusive) + * @param stop the stop index (exclusive). {@literal 0} indicates unbounded. + * @return a new {@link JsonArrayRange} + */ + public static JsonArrayRange of(long start, long stop) { + + if (start == 0 && stop == 0) { + return UNBOUNDED; + } + return new JsonArrayRange(start, stop); + } + + /** + * Create a new {@link JsonArrayRange} with no bounds. + * + * @return a new {@link JsonArrayRange} + */ + public static JsonArrayRange unbounded() { + return UNBOUNDED; + } + + /** + * Create a new {@link JsonArrayRange} with the given start index. + * + * @param start the start index (inclusive) + * @return a new {@link JsonArrayRange} + */ + public static JsonArrayRange from(long start) { + + if (start == 0) { + return UNBOUNDED; + } + return new JsonArrayRange(start, 0); + } + + /** + * Create a new {@link JsonArrayRange} with the given stop index. + * + * @param stop the stop index (exclusive). {@literal 0} indicates unbounded. + * @return a new {@link JsonArrayRange} + */ + public static JsonArrayRange to(long stop) { + + if (stop == 0) { + return UNBOUNDED; + } + return new JsonArrayRange(0, stop); + } + + public long getStart() { + return start; + } + + public long getStop() { + return stop; + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JsonArrayRange that = (JsonArrayRange) o; + return start == that.start && stop == that.stop; + } + + @Override + public int hashCode() { + return 31 * Long.hashCode(start) + Long.hashCode(stop); + } + + @Override + public String toString() { + return "[" + start + ":" + stop + "]"; + } + +} From fd590b93474ca231a55cfea19ea85154d39a497a Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Fri, 13 Mar 2026 17:42:43 +0200 Subject: [PATCH 3/4] Added Java Doc for JsonPath. Signed-off-by: Yordan Tsintsov --- .../data/redis/core/json/JsonPath.java | 119 +++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) 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 index 48876860da..a4379f2493 100644 --- a/src/main/java/org/springframework/data/redis/core/json/JsonPath.java +++ b/src/main/java/org/springframework/data/redis/core/json/JsonPath.java @@ -24,9 +24,25 @@ import java.util.stream.Collectors; /** - * JSON path. + * Immutable value object representing a JSONPath expression for use with Redis JSON operations. + *

+ * Provides a fluent API to construct JSONPath expressions in a type-safe manner. + * Use {@link #root()} as a starting point for path construction. + * + *

Examples

+ *
+ * JsonPath.root().child("address").child("city");  // $.address.city
+ * JsonPath.root().child("items").index(0);         // $.items[0]
+ * JsonPath.root().child("items").last();           // $.items[-1]
+ * JsonPath.root().child("store").wildcard();       // $.store.*
+ * JsonPath.root().recursive().child("price");      // $..price
+ * JsonPath.root().child("items").slice(1, 4);      // $.items[1:4]
+ * JsonPath.root().child("items")
+ *     .filter(FilterExpression.of("@.price < 10")); // $.items[?(@.price < 10)]
+ * 
* * @author Yordan Tsintsov + * @see Redis JSONPath Documentation * @since 4.3 */ @NullMarked @@ -41,10 +57,21 @@ 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"); @@ -52,6 +79,12 @@ public static JsonPath of(String path) { return new JsonPath(path); } + /** + * Appends a child property access ({@code .child}). + * + * @param child the property name, must not be empty + * @return a new {@link JsonPath} + */ public JsonPath child(String child) { Assert.hasText(child, "Child must not be empty"); @@ -59,38 +92,90 @@ public JsonPath child(String child) { return new JsonPath(path + "." + child); } + /** + * Appends an array index access ({@code [index]}). + * + * @param index the array index (negative values count from end) + * @return a new {@link JsonPath} + */ public JsonPath index(int index) { return new JsonPath(path + "[" + index + "]"); } + /** + * Appends access to the first array element ({@code [0]}). + * + * @return a new {@link JsonPath} + */ public JsonPath first() { return new JsonPath(path + "[0]"); } + /** + * Appends access to the last array element ({@code [-1]}). + * + * @return a new {@link JsonPath} + */ public JsonPath last() { return new JsonPath(path + "[-1]"); } + /** + * Appends recursive descent ({@code ..}). + * + * @return a new {@link JsonPath} + */ public JsonPath recursive() { return new JsonPath(path + ".."); } + /** + * Appends a wildcard for all properties ({@code .*}). + * + * @return a new {@link JsonPath} + */ public JsonPath wildcard() { return new JsonPath(path + ".*"); } + /** + * Appends a wildcard for all array elements ({@code [*]}). + * + * @return a new {@link JsonPath} + */ public JsonPath allElements() { return new JsonPath(path + "[*]"); } + /** + * Appends an array slice from {@code start} to end ({@code [start:]}). + * + * @param start the start index (inclusive) + * @return a new {@link JsonPath} + */ public JsonPath slice(int start) { return new JsonPath(path + "[" + start + ":]"); } + /** + * Appends an array slice ({@code [start:end]}). + * + * @param start the start index (inclusive) + * @param end the end index (exclusive) + * @return a new {@link JsonPath} + */ public JsonPath slice(int start, int end) { return new JsonPath(path + "[" + start + ":" + end + "]"); } + /** + * Appends an array slice with step ({@code [start:end:step]}). + * + * @param start the start index (inclusive) + * @param end the end index (exclusive) + * @param step the step value, must not be zero + * @return a new {@link JsonPath} + */ public JsonPath slice(int start, int end, int step) { Assert.isTrue(step != 0, "Step must not be zero"); @@ -98,6 +183,12 @@ public JsonPath slice(int start, int end, int step) { return new JsonPath(path + "[" + start + ":" + end + ":" + step + "]"); } + /** + * Appends access to multiple array indices ({@code [0,2,4]}). + * + * @param indices the array indices + * @return a new {@link JsonPath} + */ public JsonPath indices(int... indices) { Assert.isTrue(indices.length > 0, "Indices must not be empty"); @@ -109,6 +200,12 @@ public JsonPath indices(int... indices) { return new JsonPath(path + "[" + joined + "]"); } + /** + * Appends access to multiple named children ({@code ['a','b']}). + * + * @param names the property names + * @return a new {@link JsonPath} + */ public JsonPath children(String... names) { Assert.notEmpty(names, "Names must not be empty"); @@ -120,10 +217,21 @@ public JsonPath children(String... names) { return new JsonPath(path + "[" + joined + "]"); } + /** + * Returns the path as a string. + * + * @return the path expression + */ public String getPath() { return path; } + /** + * Appends a filter expression ({@code [?(expression)]}). + * + * @param filter the filter expression + * @return a new {@link JsonPath} + */ public JsonPath filter(FilterExpression filter) { return new JsonPath(path + filter); } @@ -151,6 +259,9 @@ public String toString() { return path; } + /** + * A filter expression for JSONPath queries (e.g., {@code @.price < 10}). + */ public static final class FilterExpression { private final String expression; @@ -159,6 +270,12 @@ private FilterExpression(String expression) { this.expression = expression; } + /** + * Creates a filter expression from the given string. + * + * @param expression the filter expression (e.g., {@code "@.price < 10"}) + * @return a new {@link FilterExpression} + */ public static FilterExpression of(String expression) { Assert.hasText(expression, "Expression must not be empty"); From 9ca78f344d8a95e27a0aff464d502e829796d8e1 Mon Sep 17 00:00:00 2001 From: Yordan Tsintsov Date: Wed, 18 Mar 2026 12:21:55 +0200 Subject: [PATCH 4/4] Addressed comments. Signed-off-by: Yordan Tsintsov --- .../redis/connection/RedisJsonCommands.java | 61 +++-- .../data/redis/core/JsonOperations.java | 6 +- .../data/redis/core/json/JsonArrayRange.java | 123 ---------- .../data/redis/core/json/JsonPath.java | 214 ------------------ 4 files changed, 33 insertions(+), 371 deletions(-) delete mode 100644 src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java diff --git a/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java index 0c1150783b..3669c834db 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisJsonCommands.java @@ -16,6 +16,7 @@ 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; @@ -29,8 +30,6 @@ */ public interface RedisJsonCommands { - String ROOT_PATH = "$"; - /** * Append the JSON values into the array at path after the last element in it. * @@ -41,7 +40,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRAPPEND * @since 4.3 */ - List<@Nullable Long> jsonArrAppend(byte[] key, String path, String... values); + List<@Nullable Long> jsonArrAppend(byte[] key, JsonPath path, String... values); /** * Search for the first occurrence of a JSON value in an array. @@ -53,7 +52,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINDEX * @since 4.3 */ - default List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value) { + 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"); @@ -73,7 +72,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINDEX * @since 4.3 */ - default List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value, long start) { + 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"); @@ -94,7 +93,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINDEX * @since 4.3 */ - List<@Nullable Long> jsonArrIndex(byte[] key, String path, String value, long start, long stop); + 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}. @@ -107,7 +106,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRINSERT * @since 4.3 */ - List<@Nullable Long> jsonArrInsert(byte[] key, String path, int index, String... values); + List<@Nullable Long> jsonArrInsert(byte[] key, JsonPath path, int index, String... values); /** * Get the length of the array at the given path. @@ -118,7 +117,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRLEN * @since 4.3 */ - List<@Nullable Long> jsonArrLen(byte[] key, String path); + List<@Nullable Long> jsonArrLen(byte[] key, JsonPath path); /** * Pop and return the last value in the array at the specified path. @@ -129,7 +128,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRPOP * @since 4.3 */ - default List<@Nullable String> jsonArrPop(byte[] key, String path) { + 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"); @@ -147,7 +146,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRPOP * @since 4.3 */ - List<@Nullable String> jsonArrPop(byte[] key, String path, int index); + List<@Nullable String> jsonArrPop(byte[] key, JsonPath path, int index); /** * Trim the array at the given path to the given range. @@ -160,7 +159,7 @@ public interface RedisJsonCommands { * @see Redis Documentation: JSON.ARRTRIM * @since 4.3 */ - List<@Nullable Long> jsonArrTrim(byte[] key, String path, int start, int stop); + 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. @@ -174,7 +173,7 @@ default Long jsonClear(byte[] key) { Assert.notNull(key, "Key must not be null"); - return jsonClear(key, ROOT_PATH); + return jsonClear(key, JsonPath.root()); } /** @@ -186,7 +185,7 @@ default Long jsonClear(byte[] key) { * @see Redis Documentation: JSON.CLEAR * @since 4.3 */ - Long jsonClear(byte[] key, String path); + Long jsonClear(byte[] key, JsonPath path); /** * Delete the JSON value at the given key. @@ -200,7 +199,7 @@ default Long jsonDel(byte[] key) { Assert.notNull(key, "Key must not be null"); - return jsonDel(key, ROOT_PATH); + return jsonDel(key, JsonPath.root()); } /** @@ -212,7 +211,7 @@ default Long jsonDel(byte[] key) { * @see Redis Documentation: JSON.DEL * @since 4.3 */ - Long jsonDel(byte[] key, String path); + Long jsonDel(byte[] key, JsonPath path); /** * Get the JSON values at the given key. @@ -226,7 +225,7 @@ default Long jsonDel(byte[] key) { Assert.notNull(key, "Key must not be null"); - List<@Nullable String> result = jsonGet(key, ROOT_PATH); + List<@Nullable String> result = jsonGet(key, JsonPath.root()); return result.isEmpty() ? null : result.getFirst(); } @@ -240,7 +239,7 @@ default Long jsonDel(byte[] key) { * @see Redis Documentation: JSON.GET * @since 4.3 */ - List<@Nullable String> jsonGet(byte[] key, String... paths); + List<@Nullable String> jsonGet(byte[] key, JsonPath... paths); /** * Merge the JSON value at the given {@code key}. @@ -256,7 +255,7 @@ 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, ROOT_PATH, value); + return jsonMerge(key, JsonPath.root(), value); } /** @@ -269,7 +268,7 @@ default Boolean jsonMerge(byte[] key, String value) { * @see Redis Documentation: JSON.MERGE * @since 4.3 */ - Boolean jsonMerge(byte[] key, String path, String value); + Boolean jsonMerge(byte[] key, JsonPath path, String value); /** * Get the JSON values at the given keys. @@ -284,7 +283,7 @@ default Boolean jsonMerge(byte[] key, String value) { Assert.notEmpty(keys, "Keys must not be empty"); Assert.noNullElements(keys, "Keys must not be null"); - return jsonMGet(ROOT_PATH, keys); + return jsonMGet(JsonPath.root(), keys); } /** @@ -296,7 +295,7 @@ default Boolean jsonMerge(byte[] key, String value) { * @see Redis Documentation: JSON.MGET * @since 4.3 */ - List<@Nullable String> jsonMGet(String path, byte[]... keys); + List<@Nullable String> jsonMGet(JsonPath path, byte[]... keys); /** * Set the JSON values at the given keys and paths. @@ -318,7 +317,7 @@ default Boolean jsonMerge(byte[] key, String value) { * @see Redis Documentation: JSON.NUMINCRBY * @since 4.3 */ - List<@Nullable Number> jsonNumIncrBy(byte[] key, String path, Number number); + List<@Nullable Number> jsonNumIncrBy(byte[] key, JsonPath path, Number number); /** * Set the JSON value at the given key. @@ -333,7 +332,7 @@ default Boolean jsonSet(byte[] key, String value) { Assert.notNull(key, "Key must not be null"); - return jsonSet(key, ROOT_PATH, value, JsonSetOption.upsert()); + return jsonSet(key, JsonPath.root(), value, JsonSetOption.upsert()); } /** @@ -347,7 +346,7 @@ default Boolean jsonSet(byte[] key, String value) { * @see Redis Documentation: JSON.SET * @since 4.3 */ - Boolean jsonSet(byte[] key, String path, String value, JsonSetOption option); + Boolean jsonSet(byte[] key, JsonPath path, String value, JsonSetOption option); /** * Append the string JSON value into the string at path after the last character. @@ -359,7 +358,7 @@ default Boolean jsonSet(byte[] key, String value) { * @see Redis Documentation: JSON.STRAPPEND * @since 4.3 */ - List<@Nullable Long> jsonStrAppend(byte[] key, String path, String value); + List<@Nullable Long> jsonStrAppend(byte[] key, JsonPath path, String value); /** * Get the string length at the given path. @@ -370,7 +369,7 @@ default Boolean jsonSet(byte[] key, String value) { * @see Redis Documentation: JSON.STRLEN * @since 4.3 */ - List<@Nullable Long> jsonStrLen(byte[] key, String path); + List<@Nullable Long> jsonStrLen(byte[] key, JsonPath path); /** * Toggle boolean values at the given key and path. @@ -381,7 +380,7 @@ default Boolean jsonSet(byte[] key, String value) { * @see Redis Documentation: JSON.TOGGLE * @since 4.3 */ - List<@Nullable Boolean> jsonToggle(byte[] key, String path); + List<@Nullable Boolean> jsonToggle(byte[] key, JsonPath path); /** * Get the JSON type at the given key. @@ -395,7 +394,7 @@ default List jsonType(byte[] key) { Assert.notNull(key, "Key must not be null"); - return jsonType(key, ROOT_PATH); + return jsonType(key, JsonPath.root()); } /** @@ -407,7 +406,7 @@ default List jsonType(byte[] key) { * @see Redis Documentation: JSON.TYPE * @since 4.3 */ - List jsonType(byte[] key, String path); + List jsonType(byte[] key, JsonPath path); /** * {@code JSON.SET} command arguments for {@code NX}, {@code XX}. @@ -466,7 +465,7 @@ public static JsonSetOption ifPathExists() { * @param value the value to set. * @since 4.3 */ - record JsonMSetArgs(byte[] key, String path, String value) { + record JsonMSetArgs(byte[] key, JsonPath path, String value) { public JsonMSetArgs { Assert.notNull(key, "Key must not be null"); @@ -475,7 +474,7 @@ record JsonMSetArgs(byte[] key, String path, String value) { } public JsonMSetArgs(byte[] key, String value) { - this(key, ROOT_PATH, value); + this(key, JsonPath.root(), value); } } diff --git a/src/main/java/org/springframework/data/redis/core/JsonOperations.java b/src/main/java/org/springframework/data/redis/core/JsonOperations.java index ab2b0766ba..7cf11f7ba1 100644 --- a/src/main/java/org/springframework/data/redis/core/JsonOperations.java +++ b/src/main/java/org/springframework/data/redis/core/JsonOperations.java @@ -23,7 +23,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.data.redis.core.json.JsonArrayRange; +import org.springframework.data.domain.Range; import org.springframework.data.redis.core.json.JsonPath; import org.springframework.util.Assert; @@ -73,7 +73,7 @@ public interface JsonOperations { Assert.notNull(key, "Key must not be null"); Assert.notNull(path, "Path must not be null"); - return arrayIndex(key, path, value, JsonArrayRange.unbounded()); + return arrayIndex(key, path, value, Range.unbounded()); } /** @@ -89,7 +89,7 @@ public interface JsonOperations { * {@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 JsonArrayRange range); + 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}. diff --git a/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java b/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java deleted file mode 100644 index ada55d9ce2..0000000000 --- a/src/main/java/org/springframework/data/redis/core/json/JsonArrayRange.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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; - -/** - * Value object representing a range of array elements. - * - * @author Yordan Tsintsov - * @since 4.3 - * @see Redis JSON.ARRINDEX - */ -@NullMarked -public final class JsonArrayRange { - - private static final JsonArrayRange UNBOUNDED = new JsonArrayRange(0, 0); - - private final long start; - private final long stop; - - private JsonArrayRange(long start, long stop) { - this.start = start; - this.stop = stop; - } - - /** - * Create a new {@link JsonArrayRange} with the given start and stop indices. - * - * @param start the start index (inclusive) - * @param stop the stop index (exclusive). {@literal 0} indicates unbounded. - * @return a new {@link JsonArrayRange} - */ - public static JsonArrayRange of(long start, long stop) { - - if (start == 0 && stop == 0) { - return UNBOUNDED; - } - return new JsonArrayRange(start, stop); - } - - /** - * Create a new {@link JsonArrayRange} with no bounds. - * - * @return a new {@link JsonArrayRange} - */ - public static JsonArrayRange unbounded() { - return UNBOUNDED; - } - - /** - * Create a new {@link JsonArrayRange} with the given start index. - * - * @param start the start index (inclusive) - * @return a new {@link JsonArrayRange} - */ - public static JsonArrayRange from(long start) { - - if (start == 0) { - return UNBOUNDED; - } - return new JsonArrayRange(start, 0); - } - - /** - * Create a new {@link JsonArrayRange} with the given stop index. - * - * @param stop the stop index (exclusive). {@literal 0} indicates unbounded. - * @return a new {@link JsonArrayRange} - */ - public static JsonArrayRange to(long stop) { - - if (stop == 0) { - return UNBOUNDED; - } - return new JsonArrayRange(0, stop); - } - - public long getStart() { - return start; - } - - public long getStop() { - return stop; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JsonArrayRange that = (JsonArrayRange) o; - return start == that.start && stop == that.stop; - } - - @Override - public int hashCode() { - return 31 * Long.hashCode(start) + Long.hashCode(stop); - } - - @Override - public String toString() { - return "[" + start + ":" + stop + "]"; - } - -} 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 index a4379f2493..9c7d0f5405 100644 --- a/src/main/java/org/springframework/data/redis/core/json/JsonPath.java +++ b/src/main/java/org/springframework/data/redis/core/json/JsonPath.java @@ -19,27 +19,10 @@ import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; -import java.util.Arrays; import java.util.Objects; -import java.util.stream.Collectors; /** * Immutable value object representing a JSONPath expression for use with Redis JSON operations. - *

- * Provides a fluent API to construct JSONPath expressions in a type-safe manner. - * Use {@link #root()} as a starting point for path construction. - * - *

Examples

- *
- * JsonPath.root().child("address").child("city");  // $.address.city
- * JsonPath.root().child("items").index(0);         // $.items[0]
- * JsonPath.root().child("items").last();           // $.items[-1]
- * JsonPath.root().child("store").wildcard();       // $.store.*
- * JsonPath.root().recursive().child("price");      // $..price
- * JsonPath.root().child("items").slice(1, 4);      // $.items[1:4]
- * JsonPath.root().child("items")
- *     .filter(FilterExpression.of("@.price < 10")); // $.items[?(@.price < 10)]
- * 
* * @author Yordan Tsintsov * @see Redis JSONPath Documentation @@ -79,144 +62,6 @@ public static JsonPath of(String path) { return new JsonPath(path); } - /** - * Appends a child property access ({@code .child}). - * - * @param child the property name, must not be empty - * @return a new {@link JsonPath} - */ - public JsonPath child(String child) { - - Assert.hasText(child, "Child must not be empty"); - - return new JsonPath(path + "." + child); - } - - /** - * Appends an array index access ({@code [index]}). - * - * @param index the array index (negative values count from end) - * @return a new {@link JsonPath} - */ - public JsonPath index(int index) { - return new JsonPath(path + "[" + index + "]"); - } - - /** - * Appends access to the first array element ({@code [0]}). - * - * @return a new {@link JsonPath} - */ - public JsonPath first() { - return new JsonPath(path + "[0]"); - } - - /** - * Appends access to the last array element ({@code [-1]}). - * - * @return a new {@link JsonPath} - */ - public JsonPath last() { - return new JsonPath(path + "[-1]"); - } - - /** - * Appends recursive descent ({@code ..}). - * - * @return a new {@link JsonPath} - */ - public JsonPath recursive() { - return new JsonPath(path + ".."); - } - - /** - * Appends a wildcard for all properties ({@code .*}). - * - * @return a new {@link JsonPath} - */ - public JsonPath wildcard() { - return new JsonPath(path + ".*"); - } - - /** - * Appends a wildcard for all array elements ({@code [*]}). - * - * @return a new {@link JsonPath} - */ - public JsonPath allElements() { - return new JsonPath(path + "[*]"); - } - - /** - * Appends an array slice from {@code start} to end ({@code [start:]}). - * - * @param start the start index (inclusive) - * @return a new {@link JsonPath} - */ - public JsonPath slice(int start) { - return new JsonPath(path + "[" + start + ":]"); - } - - /** - * Appends an array slice ({@code [start:end]}). - * - * @param start the start index (inclusive) - * @param end the end index (exclusive) - * @return a new {@link JsonPath} - */ - public JsonPath slice(int start, int end) { - return new JsonPath(path + "[" + start + ":" + end + "]"); - } - - /** - * Appends an array slice with step ({@code [start:end:step]}). - * - * @param start the start index (inclusive) - * @param end the end index (exclusive) - * @param step the step value, must not be zero - * @return a new {@link JsonPath} - */ - public JsonPath slice(int start, int end, int step) { - - Assert.isTrue(step != 0, "Step must not be zero"); - - return new JsonPath(path + "[" + start + ":" + end + ":" + step + "]"); - } - - /** - * Appends access to multiple array indices ({@code [0,2,4]}). - * - * @param indices the array indices - * @return a new {@link JsonPath} - */ - public JsonPath indices(int... indices) { - - Assert.isTrue(indices.length > 0, "Indices must not be empty"); - - String joined = Arrays.stream(indices) - .mapToObj(String::valueOf) - .collect(Collectors.joining(",")); - - return new JsonPath(path + "[" + joined + "]"); - } - - /** - * Appends access to multiple named children ({@code ['a','b']}). - * - * @param names the property names - * @return a new {@link JsonPath} - */ - public JsonPath children(String... names) { - - Assert.notEmpty(names, "Names must not be empty"); - - String joined = Arrays.stream(names) - .map(n -> "'" + n + "'") - .collect(Collectors.joining(",")); - - return new JsonPath(path + "[" + joined + "]"); - } - /** * Returns the path as a string. * @@ -226,16 +71,6 @@ public String getPath() { return path; } - /** - * Appends a filter expression ({@code [?(expression)]}). - * - * @param filter the filter expression - * @return a new {@link JsonPath} - */ - public JsonPath filter(FilterExpression filter) { - return new JsonPath(path + filter); - } - @Override public boolean equals(@Nullable Object o) { @@ -259,53 +94,4 @@ public String toString() { return path; } - /** - * A filter expression for JSONPath queries (e.g., {@code @.price < 10}). - */ - public static final class FilterExpression { - - private final String expression; - - private FilterExpression(String expression) { - this.expression = expression; - } - - /** - * Creates a filter expression from the given string. - * - * @param expression the filter expression (e.g., {@code "@.price < 10"}) - * @return a new {@link FilterExpression} - */ - public static FilterExpression of(String expression) { - - Assert.hasText(expression, "Expression must not be empty"); - - return new FilterExpression(expression); - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FilterExpression that = (FilterExpression) o; - return Objects.equals(expression, that.expression); - } - - @Override - public int hashCode() { - return Objects.hashCode(expression); - } - - @Override - public String toString() { - return "[?(" + expression + ")]"; - } - - } - }