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.

Intermediate155–185 minutesTyped JSON mutation labRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

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.

01

Mutate JSON numbers with JSON.NUMINCRBY while preserving numeric type semantics.

02

Append to strings and arrays and interpret multi-match return values.

03

Add, update, delete, and merge object members without replacing unrelated siblings.

04

Diagnose wrong-path and wrong-type mutations using JSON.TYPE and before/after evidence.

05

Separate single-command atomicity from multi-command business transactions and client retry safety.

Exact lab baseline

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.

redis-cli · deterministic cart fixture
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.

redis-cli · increment a numeric field
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.

redis-cli · deliberate numeric type failure and repair
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.

redis-cli · append to one nested JSON string
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.

redis-cli · append promotions and observe length
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.

redis-cli · multi-match numeric mutation
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.

redis-cli · child-level object updates
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.

redis-cli · merge object fields without root replacement
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.

redis-cli · remove one optional member
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.

Guarantee boundary

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.

text · race-prone pattern
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.

redis-cli · compare root and path payloads
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.

redis-cli · typed mutation acceptance path
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

  1. What does JSON.NUMINCRBY guarantee that GET/edit/SET does not?
  2. Why does JSON.STRAPPEND need a JSON string literal?
  3. What happens when JSON.MERGE receives null for an existing object member?
  4. Does one atomic JSON command make a multi-system checkout atomic?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.