Chapter 08 · Redis JSON and Document-Oriented Data
Atomic Numeric, String, Array, and Object Mutations Without Replacing Whole Documents
Mutate numeric, string, array, and object paths in Redis JSON atomically while preserving type, path, retry, and workflow boundaries.
Learning outcomes
AtlasMart now has a cart document whose quantity, note, promotion array, and fulfillment object change independently. Re-serializing the entire cart for every tiny update increases network payload and creates avoidable read/modify/write race windows. Redis JSON provides typed commands that mutate selected values atomically at the Redis command level.
Mutate JSON numbers with JSON.NUMINCRBY while preserving numeric type semantics.
Append to strings and arrays and interpret multi-match return values.
Add, update, delete, and merge object members without replacing unrelated siblings.
Diagnose wrong-path and wrong-type mutations using JSON.TYPE and before/after evidence.
Separate single-command atomicity from multi-command business transactions and client retry safety.
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. Start from one cart document
Each mutation below targets a small piece of the same bounded document.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:cart:77docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$' '{"schemaVersion":1,"itemCount":2,"note":"Leave at desk","promotions":["WELCOME"],"fulfillment":{"method":"courier","status":"open"},"items":[{"sku":"A","qty":1},{"sku":"B","qty":1}]}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$'
2. Numeric mutation: type matters
JSON.NUMINCRBY adds a numeric value to every
numeric location selected by the path. It is one Redis command,
so the path mutation itself is atomic relative to other
commands. It is not a decimal-money type and should not be used
to invent exact financial arithmetic guarantees.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:cart:77 '$.itemCount'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:cart:77 '$.itemCount' 1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.itemCount'
On the modern dollar path, the command returns the new matched value(s). RESP2/RESP3 and client wrappers can expose the array differently, so application code should test the client-level type contract.
3. Wrong type is a real error boundary
If a producer drifts itemCount into a JSON string,
numeric mutation is no longer valid.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$.itemCount' '"3"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:cart:77 '$.itemCount' 1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:cart:77 '$.itemCount'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$.itemCount' '3'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:cart:77 '$.itemCount' 1
Repair the data contract; do not silently coerce arbitrary strings in critical code.
4. String mutation appends JSON strings, not raw shell text
JSON.STRAPPEND appends a JSON string value and
returns resulting length(s). The value argument itself must be
valid JSON, so its surrounding double quotes are part of the
JSON value even though the host shell uses single quotes around
the argument.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.STRAPPEND atlasmart:ch08:cart:77 '$.note' '"; call on arrival"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.note'
Expected state: the note keeps its old text and gains the suffix. A path that matches non-string values returns null elements/errors according to the command contract; it does not stringify every JSON type for you.
5. Arrays can grow without replacing the root
JSON.ARRAPPEND appends one or more valid JSON
values to each selected array and reports resulting lengths.
This avoids fetching and re-sending the whole document just to
add one element.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRAPPEND atlasmart:ch08:cart:77 '$.promotions' '"FREESHIP"' '"VIP"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRLEN atlasmart:ch08:cart:77 '$.promotions'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.promotions[*]'
Array growth still consumes memory and increases serialization cost. “Partial update” does not mean “unbounded array is cheap.”
6. Multi-match mutation is powerful and easy to over-broaden
A wildcard can mutate several matching paths in one command.
Here, every qty below items is
incremented.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:cart:77 '$.items[*].qty' 1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.items[*].qty'
This proves one path can target multiple locations. It does not mean broad recursive paths are always safe; a schema evolution that introduces another matching field can unexpectedly expand the mutation set. Keep write paths deliberately narrow.
7. Object child writes preserve unrelated siblings
For simple object changes, write the selected child. This
preserves siblings under fulfillment.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$.fulfillment.status' '"packed"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$.fulfillment.zone' '"central"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.fulfillment'
A whole-object replacement at $.fulfillment could
accidentally erase method while changing only
status. Use the narrowest mutation that matches the
business change.
8. JSON.MERGE applies JSON Merge Patch semantics
JSON.MERGE follows RFC 7396-style merge-patch
behavior. Non-null object members update/add; a null member
deletes an existing object member; arrays are replaced as units
rather than element-wise merged.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.MERGE atlasmart:ch08:cart:77 '$.fulfillment' '{"status":"ready","pickupCode":"K7Q2"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.fulfillment'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.MERGE atlasmart:ch08:cart:77 '$.fulfillment' '{"pickupCode":null}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.fulfillment'
The second merge deletes pickupCode. This is
useful, but only when developers understand merge-patch null
semantics.
9. JSON.DEL removes selected values
JSON.DEL can remove one selected member instead of
rewriting its parent object. Root deletion removes the whole
Redis JSON value, so keep destructive paths explicit.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:cart:77 '$.temporaryNote' '"debug-only"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.DEL atlasmart:ch08:cart:77 '$.temporaryNote'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.temporaryNote'
10. Atomic command does not equal atomic workflow
Each JSON mutation command executes atomically relative to other Redis commands. A workflow such as “increment item count, append a promotion, update inventory elsewhere, charge payment, then mark packed” spans multiple commands and external systems. A crash between steps can leave a valid Redis JSON document that is nevertheless inconsistent with business state.
Use single-command mutations for single-path invariants. For multi-key/multi-system workflows, design idempotency, optimistic concurrency, transactions/functions where appropriate, and reconciliation. Chapter 13 revisits Redis transactions explicitly.
11. Wrong approach: GET root, edit locally, SET root for every tiny change
The read/modify/write pattern transfers the whole document twice and can overwrite a concurrent narrow update if two clients start from the same old root.
Client A: JSON.GET cart '$'Client B: JSON.GET cart '$'Client A: changes note locally; JSON.SET cart '$' whole-document-AClient B: changes status locally; JSON.SET cart '$' whole-document-BResult: B can erase A's note change even though the business changes were independent.
Prefer path-level commands when the mutation is naturally local. If the update must compare an expected version, add explicit concurrency control rather than assuming partial commands solve every race.
12. Network and memory evidence
Measure the difference between retrieving the root and only the field a request needs. Small fixtures will not predict production latency, but they make payload amplification visible.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:cart:77 '$.fulfillment.status'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:cart:77
For realistic capacity work, record serialized bytes, response bytes, network round trips, p50/p95/p99 latency, document size distribution, and concurrent mutation patterns.
13. Reproducible mutation lab
Run a clean fixture and prove one mutation from each family.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:lesson2 '$' '{"count":1,"label":"box","tags":[],"meta":{"state":"new"}}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:lesson2 '$.count' 2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.STRAPPEND atlasmart:ch08:lesson2 '$.label' '"-A"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.ARRAPPEND atlasmart:ch08:lesson2 '$.tags' '"fragile"'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.MERGE atlasmart:ch08:lesson2 '$.meta' '{"state":"packed","worker":"w1"}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:lesson2 '$'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson2
Acceptance state before cleanup: count=3, label
box-A, one fragile tag, and meta
containing state=packed plus
worker=w1.
14. Production judgment
Typed JSON commands are strongest when the application owns a clear schema and changes bounded sub-elements. Verify wrong-type behavior, multi-match breadth, array growth, root TTL, replication/persistence costs, retry semantics, and client decoding. Avoid financial precision assumptions on generic JSON numbers. Treat a large wildcard update or huge array operation as potentially expensive even though it is one atomic command.
15. Summary and next step
Redis JSON gives you server-side partial mutation without turning the document into a schema-managed database. Next, we decide what should be embedded, duplicated, split into keys, versioned, or bounded.
Check your understanding
- What does JSON.NUMINCRBY guarantee that GET/edit/SET does not?
- Why does JSON.STRAPPEND need a JSON string literal?
- What happens when JSON.MERGE receives null for an existing object member?
- Does one atomic JSON command make a multi-system checkout atomic?
- Why can a broad wildcard write become risky after schema evolution?
Review the answers
The selected numeric mutation executes as one Redis command, avoiding a client-side read/modify/write race for that field.
The command accepts JSON values, so a string must be encoded as a valid JSON string.
The member is deleted under JSON Merge Patch semantics.
No. External side effects and multiple commands need separate correctness design.
New matching fields can cause the same path to mutate more locations than the original code expected.
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