JSON
|
This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client
libraries. JSON has been a native Redis data type since Redis 8.0 — what used to ship as the separate
RedisJSON module is now built into redis:8 itself, addressable through the same JSON.* command family and
indexable by Redis Query Engine without any extra setup. A JSON value lives under an ordinary Redis key, is
read and written by path rather than as one opaque blob, and can be nested (objects, arrays) in a way a hash’s
flat field/value pairs cannot represent.
JSON as a native type
A key holding a JSON document behaves like any other Redis key for EXPIRE, TTL, DEL, TYPE, replication,
and persistence (RDB/AOF); TYPE reports it as ReJSON-RL. What sets it apart from a string holding
serialized JSON text is that Redis parses the structure once and lets every JSON.* command address a part of
it directly — incrementing a nested number, appending to a nested array, or reading a handful of fields
without first pulling the whole document across the wire and parsing it client-side.
JSONPath addressing
Every JSON. command that takes a path accepts two dialects. The *legacy dialect (.field, .arr[0], no
leading $) predates JSONPath support and returns a single value — or an error if the path matches nothing
or matches more than one node in an ambiguous way. The JSONPath dialect, rooted at $, is the one worth
reaching for by default:
-
$— the document root. -
$.field,$.nested.field— a single named field, arbitrarily nested. -
$.arr[0],$.arr[-1]— an array element by index, negative indices counting from the end. -
$.arr[]— every element of an array;$.every value of an object. -
$..field— recursive descent, matchingfieldat any depth. -
$.arr[?(@.price < 15)]— a filter expression, keeping only elements matching the predicate. -
$.arr[1:3]— a slice (start:end:step, Python-slice semantics).
A command run with a JSONPath always replies with an array of matches — even when the path can only ever
match zero or one node — because unlike the legacy dialect, JSONPath is inherently a multi-match query
language. This is the main behavioural difference to keep in mind when migrating a legacy-path call to
JSONPath: JSON.GET bookshop:book:9780000000001 .price returns the bare number, while
JSON.GET bookshop:book:9780000000001 $.price returns [19.99]. See
Redis JSON path syntax.
Document commands
-
JSON.SET key path value [NX \| XX]— sets the value atpath, creating the document ifpathis$and the key doesn’t exist yet.NXonly sets if the path doesn’t already exist;XXonly if it does.valuemust be valid JSON, including a quoted string for a scalar string value. See JSON.SET. -
JSON.GET key [INDENT indent] [NEWLINE newline] [SPACE space] [path [path …]]— reads one or more paths back as JSON text; with several paths it replies with an object keyed by each path. Omittingpathreads the whole document. See JSON.GET. -
JSON.MGET key [key …] path— reads the same path from several keys in one round trip, replying with one JSON value (ornil) per key. See JSON.MGET. -
JSON.MSET key path value [key path value …]— writes to several keys and paths atomically in one call, the multi-key counterpart toJSON.SET. See JSON.MSET. -
JSON.MERGE key path value— applies an RFC 7396 JSON Merge Patch atpath: object fields present invalueoverwrite or add fields in the target, and a field set tonullinvaluedeletes it, all without having toJSON.GETthe target first to compute the merged result client-side. See JSON.MERGE. -
JSON.NUMINCRBY key path value— addsvalue(which may be negative or fractional) to the number atpath, atomically, in place. See JSON.NUMINCRBY. -
JSON.ARRAPPEND key [path] value [value …]— appends one or more values to the array atpath. See JSON.ARRAPPEND. -
JSON.STRAPPEND key [path] value— appends to the string atpath. See JSON.STRAPPEND. -
JSON.DEL key [path]— deletes the value atpath(the whole key ifpathis omitted or$), returning the number of paths deleted. See JSON.DEL. -
JSON.TYPE key [path]— reports the JSON type (object,array,string,integer,number,boolean,null) atpath, one per match. See JSON.TYPE.
Partial reads and partial updates
Every command above operates on a path, not the whole document, which is the core reason to reach for JSON
instead of a plain string holding serialized text: JSON.NUMINCRBY bookshop:book:9780000000001 $.stock -1
decrements a single nested field server-side, in one round trip, with no read-modify-write race between two
clients touching the same document — the equivalent with a JSON string key would mean GET, deserialize,
mutate, serialize, and SET back, racing any concurrent writer in between. The same applies to reads: pulling
just $.title and $.price from a large document with JSON.GET key $.title $.price avoids transferring and
parsing fields nothing needs.
RESP3 replies
Under RESP2, JSON.GET and friends reply with a JSON-formatted string that the client library parses a
second time into its own JSON representation. Under RESP3 (negotiated via HELLO 3), commands that logically
return a JSON array or object instead reply using RESP3’s own array and map types directly, so a JSON object
comes back as a native map reply rather than a string the client re-parses — one fewer parsing pass, and a
reply shape that composes naturally with other RESP3 map replies (such as JSON.GET with multiple paths,
which was already object-shaped). Jedis and Lettuce both negotiate RESP3 transparently when the server
supports it, so application code sees the client library’s normal JSON mapping either way.
Memory: homogeneous numeric arrays
A JSON array whose elements are all numbers of the same kind (all integers, or all doubles) is stored
internally in a packed, contiguous layout instead of as a generic array of boxed JSON values, which keeps a
numeric array such as an embedding vector or a list of yearly sale counts considerably cheaper to hold in RAM
than the same values expressed as a mixed-type array. The $.embedding field in the Bookshop scenario below
is exactly this shape — a flat array of floating-point numbers used for vector similarity search — and this
packed layout is what keeps a catalogue of embedded documents affordable at scale.
JSON vs. hash
Hashes already covers this trade-off from the hash side: reach for JSON when a
record is naturally nested or queried/updated by path, and for a hash when it’s a flat bag of scalar fields
read or written one at a time. The Bookshop catalogue uses JSON for the book documents themselves (nested
genres array, an embedding vector) and a hash for the cart (a flat, per-item TTL’d quantity map) precisely
along that line.
Example: loading and updating the Bookshop catalogue
Loading the three Bookshop books as JSON documents:
127.0.0.1:6379> JSON.SET bookshop:book:9780000000001 $ '{"isbn":"9780000000001","title":"The Redis Way","author":"J. Carter","genres":["fiction"],"price":19.99,"year":2023,"description":"A novel about a database that never forgets.","stock":12}'
OK
127.0.0.1:6379> JSON.SET bookshop:book:9780000000002 $ '{"isbn":"9780000000002","title":"Streams in Practice","author":"M. Duarte","genres":["technology"],"price":34.50,"year":2024,"description":"Building event-driven systems on append-only logs.","stock":7}'
OK
127.0.0.1:6379> JSON.SET bookshop:book:9780000000003 $ '{"isbn":"9780000000003","title":"Vectors for Humans","author":"S. Achebe","genres":["technology"],"price":29.00,"year":2025,"description":"An intuitive tour of embeddings and similarity search.","stock":5}'
OK
This derives from JSON.SET.
Reading the title of every book back in one round trip with JSON.MGET:
127.0.0.1:6379> JSON.MGET bookshop:book:9780000000001 bookshop:book:9780000000002 bookshop:book:9780000000003 $.title
1) "[\"The Redis Way\"]"
2) "[\"Streams in Practice\"]"
3) "[\"Vectors for Humans\"]"
This derives from JSON.MGET.
A sale decrements stock, and a separate pricing update raises the price and adds a short blurb via
JSON.MERGE:
127.0.0.1:6379> JSON.NUMINCRBY bookshop:book:9780000000001 $.stock -1
"[11]"
127.0.0.1:6379> JSON.MERGE bookshop:book:9780000000001 $ '{"price":21.99,"promo":"Staff pick"}'
OK
JSON.NUMINCRBY derives from JSON.NUMINCRBY, and the
merge from JSON.MERGE.
The same stock decrement from a Spring service, using Jedis:
package com.example.bookshop;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.json.Path2;
public class InventoryService {
private final RedisClient jedis;
public InventoryService(RedisClient jedis) {
this.jedis = jedis;
}
public void recordSale(String isbn, int quantity) {
String key = "bookshop:book:" + isbn;
jedis.jsonNumIncrBy(key, Path2.of("$.stock"), -quantity);
}
}
This wraps JSON.NUMINCRBY.
The diagram below shows one Bookshop book document alongside the JSONPath expressions used above, each pointing at the field it addresses:
Related pages
-
Hashes — the Bookshop cart hash, and when a hash fits a record better than JSON.
-
Streams, Pub/Sub & Keyspace Notifications — keyspace and subkey notifications, including per-JSON-path events.
-
Strings, Bitmaps & Bitfields — the catalogue cache string,
bookshop:cache:book:<isbn>, as a JSON-serialized alternative to a native JSON document for a short-TTL read cache.
References
-
Redis JSON — overview of the native JSON type.
-
Redis JSON path syntax — legacy path vs. JSONPath, and the supported JSONPath operators.
-
Indexing and querying JSON — indexing JSON documents with Redis Query Engine.
-
RESP3 protocol — the RESP3 reply types JSON commands use when negotiated.