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, matching field at 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 at path, creating the document if path is $ and the key doesn’t exist yet. NX only sets if the path doesn’t already exist; XX only if it does. value must 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. Omitting path reads 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 (or nil) 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 to JSON.SET. See JSON.MSET.

  • JSON.MERGE key path value — applies an RFC 7396 JSON Merge Patch at path: object fields present in value overwrite or add fields in the target, and a field set to null in value deletes it, all without having to JSON.GET the target first to compute the merged result client-side. See JSON.MERGE.

  • JSON.NUMINCRBY key path value — adds value (which may be negative or fractional) to the number at path, atomically, in place. See JSON.NUMINCRBY.

  • JSON.ARRAPPEND key [path] value [value …​] — appends one or more values to the array at path. See JSON.ARRAPPEND.

  • JSON.STRAPPEND key [path] value — appends to the string at path. See JSON.STRAPPEND.

  • JSON.DEL key [path] — deletes the value at path (the whole key if path is 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) at path, 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:

flowchart LR subgraph Doc["bookshop:book:9780000000001 (JSON)"] Root["$ (document root)"] Isbn["isbn: \"9780000000001\""] Title["title: \"The Redis Way\""] Price["price: 21.99"] Genres["genres: fiction"] Stock["stock: 11"] Promo["promo: \"Staff pick\""] Root --> Isbn Root --> Title Root --> Price Root --> Genres Root --> Stock Root --> Promo end P1["$.title"] -.-> Title P2["$.stock"] -.-> Stock P3["$.genres[*]"] -.-> Genres P4["$ (JSON.MERGE target)"] -.-> Root
  • 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