Chapter 08 · Redis JSON and Document-Oriented Data
Choose Hashes vs JSON from Query Needs, Structure, Memory, Client Ergonomics, and Search
Choose Redis Hashes, JSON, or multiple keys from structure, query, memory, lifecycle, client, Search, and migration requirements rather than slogans.
Learning outcomes
AtlasMart has three plausible representations for a small application record: one Redis Hash, one Redis JSON document, or several independent keys. None is universally superior. The correct choice follows the required structure, query patterns, mutation granularity, lifecycle, memory/cardinality, client ergonomics, security, and Search needs.
Compare Hash, JSON, and many-key models from concrete access patterns instead of feature slogans.
Measure bounded representative fixtures with MEMORY USAGE and payload evidence.
Choose JSON when nested structure/path mutation is valuable and Hash when flat field/value access is sufficient.
Recognize when many keys are justified by independent lifecycle/authorization/hotness and when they create cardinality overhead.
Design a migration/rollback decision with Search, TTL, schema, persistence, Cluster, and client consequences.
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. Begin with one workload, not one favorite data type
Consider an AtlasMart session/cart summary with customer ID, currency, item count, nested shipping address, tags, and a short TTL. The important questions are: which fields are read together, which mutate independently, whether nested arrays/objects matter, whether fields require Search, whether lifecycle differs, and how large/cardinal the data can become.
| Need | Hash | JSON | Many keys |
|---|---|---|---|
| flat field reads/writes | excellent | works, somewhat richer than needed | works but more key overhead |
| nested object/array | manual flatten/serialization | native | split relationships/application composition |
| single whole-record key TTL | yes | yes | one TTL per key |
| independent sub-value key TTL | not ordinary Hash fields on older versions; field expiry is version-specific | no independent JSON-path key TTL | yes |
| Search indexing | supported | supported with JSONPath schema | usually index chosen source keys/structures |
| key cardinality | one key | one key | many keys |
2. Build equivalent small fixtures
The fixtures are intentionally small; their purpose is to make representation differences observable, not crown a universal winner.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:choice:json atlasmart:ch08:choice:hash atlasmart:ch08:choice:customer atlasmart:ch08:choice:itemCount atlasmart:ch08:choice:citydocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:choice:json '$' '{"customerId":77,"currency":"AZN","itemCount":2,"shipping":{"city":"Baku"},"tags":["mobile","returning"]}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch08:choice:hash customerId 77 currency AZN itemCount 2 shipping.city Baku tags mobile,returningdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch08:choice:customer 77docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch08:choice:itemCount 2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch08:choice:city Baku
3. Compare direct reads
If a request needs only item count, both Hash and JSON can fetch a narrow value. Many keys can fetch it directly too.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:choice:json '$.itemCount'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HGET atlasmart:ch08:choice:hash itemCountdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch08:choice:itemCount
Client ergonomics differ: JSON preserves numeric type on decode; Hash and String values are byte strings that the application parses according to its contract.
4. Compare structured reads
When nested shipping or arrays are first-class application structure, JSON communicates the shape directly. A Hash usually flattens names or stores serialized sub-values; many keys require application composition.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:choice:json '$.shipping' '$.tags'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HMGET atlasmart:ch08:choice:hash shipping.city tags
5. Compare mutation ergonomics
Atomic scalar mutations exist in all models, but at different levels. Hash fields and separate String keys use their native numeric commands; JSON uses typed path mutation.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.NUMINCRBY atlasmart:ch08:choice:json '$.itemCount' 1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HINCRBY atlasmart:ch08:choice:hash itemCount 1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app INCR atlasmart:ch08:choice:itemCount
Do not choose solely from command brevity. Consider how the rest of the record is modeled and validated.
6. Compare memory carefully
Observe per-key memory, then remember that many-key total includes every key and its metadata.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:choice:jsondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:choice:hashdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:choice:customerdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:choice:itemCountdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:choice:citydocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DBSIZE
Repeat at realistic cardinality and distributions. Small Hashes can use compact internal encodings; JSON has tree structure; many keys pay per-key overhead. These are implementation/capacity observations, not immutable API guarantees.
7. Lifecycle can outweigh structure
If the entire session expires together, one Hash or JSON key makes TTL policy simple. If city is persistent but a verification token must disappear in 30 seconds, storing them in the same JSON key couples lifecycle. A separate key may be justified for the short-lived sensitive value.
| Lifecycle question | Implication |
|---|---|
| all fields expire together | single Hash/JSON key is operationally simple |
| one field has independent TTL | separate key or verified field-expiration feature may fit better |
| one sub-value has different security policy | separate key/service can align authorization boundary |
| unbounded child collection | separate structure avoids giant parent key |
8. Cardinality can make “many simple keys” expensive
Splitting ten fields for ten million entities into individual keys means up to one hundred million keys before auxiliary structures. That affects key metadata, scanning, operational discoverability, backups, expiration work, Cluster distribution, and monitoring. Estimate total key cardinality before choosing “one key per field” casually.
9. Search needs can favor either Hash or JSON
Redis Search can index Hash fields and JSON paths. JSON is not automatically the Search choice. Use JSON when source structure benefits from nesting/arrays/path operations; use Hash when a flat record is natural. Then build only the Search schema required by queries.
Source representation and Search schema are related decisions, but not the same decision. A flat searchable record can remain a Hash; a nested document can be JSON and index only selected paths.
10. Client ergonomics and type contracts differ
Hash values arrive as byte/string fields and require application parsing. Redis JSON preserves JSON types and nested shape, but modern JSONPath APIs often return arrays of matches, which the client must decode correctly. Many keys can map cleanly to scalar APIs but require more round trips or pipelining when a logical record spans them.
| Concern | Hash | JSON | Many keys |
|---|---|---|---|
| numeric type | application contract | JSON numeric type | application contract per key |
| nested decode | manual | native JSON | application composition |
| partial read | HGET/HMGET | JSON.GET path(s) | GET/MGET |
| multi-value response shape | flat fields | JSONPath multi-match aware | list of key replies |
11. Wrong approach: choose JSON because it looks modern
A flat five-field counter/config record may gain no value from nested JSON, while a nested catalog document may become awkward if forced into Hash flattening. The wrong criterion is aesthetics. The repair is to write down read/write/query/lifecycle/security/cardinality requirements first, then benchmark representative candidates.
12. Migration Hash → JSON has real compatibility work
A migration must decide key naming, field types, nested shape, TTL transfer, client cutover, Search source type/schema, rollback, and mixed-version reads. Simply copying strings into JSON can accidentally turn numeric fields into JSON strings.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch08:migrate:hash sku M-1 stock 7docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:migrate:json '$' '{"schemaVersion":1,"sku":"M-1","stock":7}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.TYPE atlasmart:ch08:migrate:json '$.stock'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HGET atlasmart:ch08:migrate:hash stock
Validate counts and values before switching traffic; keep rollback until the source representation can be reconstructed or remains available.
13. Production decision matrix
Use this as a reasoning checklist, not an automatic scoring formula.
| Choose... | When this dominates | Watch for |
|---|---|---|
| Hash | flat compact record, field reads/writes, simple source model | manual types/nesting; whole-hash access cost; version-sensitive field expiry |
| JSON | bounded nested/array record, path reads/writes, JSON-native client model | big documents, key-level lifecycle, schema drift, multi-match path semantics |
| Many keys | independent lifecycle/security/hotness or separate scalar access | key cardinality, network round trips, composition, scanning/operations |
| Search index | secondary query requirements across records | memory/write cost, schema migration, query/index visibility |
14. Capstone comparison lab
Rebuild small equivalent fixtures, mutate/read them, record memory, and write your decision based on workload rather than the smallest byte count in one run.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:lesson5:json atlasmart:ch08:lesson5:hash atlasmart:ch08:lesson5:name atlasmart:ch08:lesson5:stockdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.SET atlasmart:ch08:lesson5:json '$' '{"name":"Cable","stock":4,"meta":{"color":"black"}}'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HSET atlasmart:ch08:lesson5:hash name Cable stock 4 meta.color blackdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch08:lesson5:name Cabledocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch08:lesson5:stock 4docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app JSON.GET atlasmart:ch08:lesson5:json '$.meta.color'docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app HGET atlasmart:ch08:lesson5:hash meta.colordocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson5:jsondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson5:hashdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson5:namedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MEMORY USAGE atlasmart:ch08:lesson5:stock
Acceptance criteria: all representations expose the same logical values. Record the observed memory numbers, but justify your choice from structure/lifecycle/query/client needs too.
15. Cleanup and rollback discipline
Remove only Chapter 08 fixtures/indexes you created. Do not use FLUSHDB/FLUSHALL.
docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin FT.DROPINDEX atlasmart-ch08-products-idxdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app DEL atlasmart:ch08:product:1001 atlasmart:ch08:cart:77 atlasmart:ch08:product:2001 atlasmart:ch08:schema:v1 atlasmart:ch08:schema:v2 atlasmart:ch08:search:product:1 atlasmart:ch08:search:product:2 atlasmart:ch08:outside:product:3 atlasmart:ch08:compare:json atlasmart:ch08:compare:hash atlasmart:ch08:choice:json atlasmart:ch08:choice:hash atlasmart:ch08:choice:customer atlasmart:ch08:choice:itemCount atlasmart:ch08:choice:city atlasmart:ch08:migrate:hash atlasmart:ch08:migrate:json atlasmart:ch08:lesson5:json atlasmart:ch08:lesson5:hash atlasmart:ch08:lesson5:name atlasmart:ch08:lesson5:stock
An “unknown index” response is acceptable if Lesson 4 already dropped it. Cleanup remains prefix-specific and never touches unrelated data.
16. Production judgment
JSON is a strong choice for cohesive bounded nested records, not a default replacement for Hashes. Hashes remain excellent flat object-like structures; many keys are appropriate when lifecycle/authorization/hotness truly diverge. Measure realistic memory, payload, mutation latency, Search index cost, key cardinality, persistence/replication effects, and client behavior. Preserve rollback when changing representation.
17. Summary and bridge to Chapter 09
Chapter 08 established JSON as structured storage, not schema magic or automatic Search. You can now choose Hash versus JSON versus many keys based on evidence. Chapter 09 takes the Search side deeper: index scope, TEXT/TAG/NUMERIC/GEO semantics, query syntax, aggregation, profiling, and index cost.
Check your understanding
- When can Hash be a better source representation than JSON?
- When can many keys be justified despite key-cardinality overhead?
- Does JSON preserve numeric type more directly than Hash?
- Can Redis Search index both Hash and JSON sources?
- What must a Hash-to-JSON migration preserve besides values?
Review the answers
When the record is naturally flat and field-level access is sufficient.
When fields genuinely need independent lifecycle, authorization, hotness, or scalar access boundaries.
Yes. JSON has numeric values; Hash fields are bytes/strings interpreted by the application.
Yes. Source representation should be chosen from the data/workload, then Search schema from query needs.
Key naming, types, TTL/lifecycle, client compatibility, Search schema/source type, observability, and rollback.
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
- Redis Hashes — flat field/value comparison
- HSET — Hash field mutation semantics
- Redis memory optimization — memory/cardinality context
- Redis Cluster specification — key slot and distribution boundaries