Chapter 08 · Redis JSON and Document-Oriented Data
JSON.SET/GET and JSON Paths: Store and Address Nested Documents
Store and address nested Redis JSON documents with modern JSONPath semantics, explicit return-shape reasoning, and bounded AtlasMart modeling.
Learning outcomes
AtlasMart needs a product record whose dimensions, inventory summary, shipping settings, tags, and localized metadata belong together. Flattening every nested value into unrelated keys makes reads and lifecycle reasoning harder, while replacing a whole serialized blob for one nested change wastes network and write work. Redis JSON stores a JSON document under one Redis key and lets commands address values through JSONPath expressions.
Create and retrieve nested JSON documents with JSON.SET and JSON.GET.
Explain modern dollar-prefixed JSONPath versus legacy paths and their different return shapes.
Inspect JSON value types, object fields, arrays, and missing-path behavior without guessing.
Update a nested location without replacing the whole document and explain path-creation boundaries.
Keep JSON storage, Search indexing, schema validation, key TTL, and application authorization as separate concerns.
All Chapter 08 mandatory labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from Docker
Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default ACL user disabled,
named ACL users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data volume, and no explicit Redis
maxmemory limit or eviction policy. Redis 8
integrates JSON and Search capabilities in Redis Open Source,
so the pinned image is the mandatory free/local path; no
separate Redis Stack image or managed service is required. The
primary client is the redis-cli shipped in the
same image. Fixtures are bounded under
atlasmart:ch08:*.
1. Redis key outside, JSON tree inside
A Redis JSON document still lives at one ordinary Redis key. The key participates in Redis keyspace behavior—ACL key patterns, expiration, persistence, replication, Cluster slotting—while the value is a typed JSON tree. A JSON path selects locations inside that tree; it is not another Redis key and cannot have an independent Redis key TTL merely because it is nested.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:product:1001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$' '{"schemaVersion":1,"sku":"SKU-1001","name":"Trail Backpack","priceCents":12990,"inventory":{"BAK-1":12,"TBS-1":4},"dimensions":{"weightGrams":870,"widthMm":320},"tags":["outdoor","travel"]}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TYPE atlasmart:ch08:product:1001
Expected evidence: JSON.SET returns
OK; JSON.GET ... '$' returns a
JSON-encoded array containing the root match; ordinary Redis
TYPE reports the Redis JSON type rather than
pretending the value is a String.
2. Modern JSONPath starts at dollar
In this course, modern JSONPath expressions start with
$. Dot notation selects object members, brackets
select array positions, * selects multiple
children, filters select values by predicates, and recursive
descent can search deeper trees. A modern path may match zero,
one, or many locations, so commands often return an array-shaped
result even when your particular fixture happens to have one
match.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$.inventory.BAK-1'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$.tags[*]'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$..weightGrams'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:product:1001 '$.priceCents'
With the deterministic fixture, the first value is 12, the tags
are outdoor and travel, and the type
of priceCents is numeric/integer. Exact RESP2
versus RESP3 presentation can differ, so treat the semantic
values—not terminal punctuation—as the invariant.
3. Legacy paths exist, but do not mix mental models
Redis JSON retains legacy path syntax for compatibility. Legacy paths generally resolve only the first matching location and several commands return a scalar or different nil/error shape instead of a modern JSONPath array. This is one reason copied examples from old RedisJSON tutorials can look inconsistent beside Redis 8 documentation.
| Question | Modern JSONPath | Legacy path |
|---|---|---|
| Root form | $ |
. or omitted |
| Multiple matches | supported | first matching location semantics |
| Typical return shape | array/multi-match aware | often scalar/single-match |
| Course recommendation | preferred for new work | use only when maintaining existing code |
Do not “fix” an unexpected array by switching path syntaxes blindly. Choose one API contract, test it with your client library and RESP version, and decode it consistently.
4. Introspection proves what the tree actually contains
JSON documents are typed, but Redis JSON is not an application schema validator. Use introspection to observe stored state during development and operations.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:product:1001 '$.inventory'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.OBJKEYS atlasmart:ch08:product:1001 '$.inventory'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRLEN atlasmart:ch08:product:1001 '$.tags'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:product:1001 '$.missing'
The missing path should produce an empty modern-path result
rather than inventing a value. OBJKEYS is O(N) in
the object's keys, so it is useful evidence—not a free
schema-discovery call to run repeatedly on giant objects.
5. JSON.SET can mutate a path without replacing the root
Once a path's parent exists, JSON.SET can replace
that matched value or create a missing final object member. It
cannot conjure a chain of missing intermediate objects.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.dimensions.heightMm' '510'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.inventory.BAK-1' '11'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$.dimensions' '$.inventory'
The document remains one key while only selected sub-elements change. This is the core difference from storing an opaque JSON string in a normal Redis String: Redis can address and mutate the tree itself.
6. Edge case: missing intermediate path
A common mistake is assuming Redis will create every missing
object on a dotted path. It will not. If
$.shipping does not exist, setting
$.shipping.carrier cannot create the intermediate
shipping object.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.shipping.carrier' '"AZPost"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.shipping' '{"carrier":"AZPost","fragile":false}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$.shipping'
The first command returns nil because the intermediate parent is
absent. Creating the object at $.shipping makes
later child updates valid. This boundary should be covered by
tests when schema versions add new nested containers.
7. NX and XX apply to path existence
NX writes only when the target path has no match;
XX writes only when it already has a match. These
are path-condition controls, not full schema or concurrency
validation.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.rating' '4.6' NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.rating' '4.7' NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.rating' '4.7' XXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$.rating'
Use these guards when “create only” or “update only” is the actual invariant. They do not compare an expected old value; later chapters cover broader concurrency mechanisms.
8. Client-decoded value versus wire-level JSON string
JSON.GET returns serialized JSON, and client
libraries often decode that into language-native arrays,
objects, numbers, booleans, and nulls. To make the boundary
concrete without requiring a third-party package, this Python
example calls the pinned redis-cli --raw and
decodes the JSON with the standard library.
import jsonimport subprocesscmd = [ "docker", "exec", "-e", "REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026", "atlasmart-redis-ch01", "redis-cli", "--raw", "--user", "atlasmart-app", "JSON.GET", "atlasmart:ch08:product:1001", "$"]raw = subprocess.check_output(cmd, text=True).strip()matches = json.loads(raw)doc = matches[0]print(doc["sku"], doc["inventory"]["BAK-1"], type(doc["priceCents"]).__name__)
This example makes two contracts visible: Redis returns serialized JSON, and the application chooses how to decode it. A production client may expose a different convenience API, so test its exact return types instead of assuming redis-cli formatting.
9. JSON is storage; Search is a separate secondary structure
Storing name, priceCents, or
tags in JSON does not automatically create a Redis
Search index. Direct key/path access works immediately. Querying
“all products tagged outdoor under a price” requires an explicit
Search index, taught in Lesson 4 and then deeply in Chapter 09.
A JSON document is authoritative stored data for this lesson. A Search index is a derived access structure with its own schema, memory, write, visibility, and migration costs.
10. Wrong approach: use JSON as schema enforcement
Redis accepts valid JSON whose application-level shape differs
from your intended schema. For example, one producer can write
priceCents as a string while another writes an
integer.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.priceCents' '"12990"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:product:1001 '$.priceCents'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:product:1001 '$.priceCents' '12990'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:product:1001 '$.priceCents'
The repair is not “Redis JSON validates schema.” The repair is an application contract: validate on write, carry an explicit schema version, reject incompatible inputs, and test migrations.
11. Memory and size are properties to measure, not slogans
A nested JSON document has structural overhead and can become a big key if arrays or embedded objects grow without bound. Measure both Redis-key memory and serialized payload size.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:product:1001docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin JSON.DEBUG MEMORY atlasmart:ch08:product:1001 '$'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:product:1001 '$'
MEMORY USAGE observes the whole Redis key.
JSON.DEBUG MEMORY is debugging/introspection
evidence and is not an application query primitive. Neither one
creates a universal “JSON is larger/smaller than Hash” rule.
12. Reproducible lesson lab
Rebuild the fixture, prove path semantics, and leave the environment clean.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:lesson1 '$' '{"schemaVersion":1,"customer":{"id":77,"name":"Leyla"},"items":[{"sku":"A","qty":1},{"sku":"B","qty":2}]}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:lesson1 '$.customer.name'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:lesson1 '$.items[*].sku'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRLEN atlasmart:ch08:lesson1 '$.items'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson1
Acceptance criteria: nested selection returns Leyla; the wildcard returns A and B; array length is 2; memory usage is a positive environment-specific value; cleanup removes only the lesson fixture.
13. Production judgment
Redis JSON is appropriate when a bounded record naturally contains nested/array structure and your workload benefits from path reads/writes. It does not provide schema validation, field-level key TTLs, relational constraints, or free arbitrary queries. Measure document size, mutation paths, persistence/replication cost, ACL scope, client decoding, Search indexing cost, and tail latency. In Cluster, the whole document belongs to the hash slot of its Redis key; nested paths do not distribute independently.
14. Summary and next step
You can now distinguish the Redis key from its JSON tree, use modern JSONPath intentionally, interpret return shapes, introspect structure, and perform bounded nested updates. Next we mutate numbers, strings, arrays, and objects atomically without replacing the whole root document.
Check your understanding
- Why does JSON.GET with a dollar path often return an array-shaped result?
- Can JSON.SET create an entire chain of missing intermediate objects?
- Does storing JSON automatically make its fields searchable with FT.SEARCH?
- What does Redis JSON validate for you?
- Why should you avoid mixing modern and legacy paths casually?
Review the answers
A modern JSONPath may match multiple locations, so the API is multi-match aware even when one location currently matches.
No. A missing final member can be created when its parent object exists; missing intermediate parents must be created first.
No. Search requires an explicit index and schema.
JSON syntax/types at storage operations, not your complete application schema or business invariants.
They have different matching and return/error/null semantics, which can silently change client behavior.
Authoritative references
- Redis JSON data type — JSON storage, typed operations, and command overview
- JSONPath syntax — modern dollar-path selection, wildcards, filters, slices, and recursive descent
- JSON.SET — root/path writes plus NX/XX and path-creation rules
- JSON.GET — serialized return shapes for modern and legacy paths
- JSON.TYPE — type introspection and modern/legacy return behavior
- JSON.OBJKEYS — object-key introspection and complexity
- JSON.ARRLEN — array length and multi-match behavior
- JSON.NUMINCRBY — atomic numeric path mutation
- JSON.STRAPPEND — atomic string append and resulting lengths
- JSON.ARRAPPEND — array append and resulting lengths
- JSON.MERGE — RFC 7396 merge-patch behavior for objects and arrays
- Index JSON documents — FT.CREATE ON JSON, JSONPath schema attributes, and indexing behavior
- FT.CREATE — Search index schema, prefix scope, and field types
- FT.INFO — index metadata and observable indexing state
- MEMORY USAGE — per-key memory evidence and sampling
- Redis 8.10 release notes — pinned server release family