Redis JSON Module: JSON.SET, JSONPath Queries & Node.js Integration

Last updated:

RedisJSON (Redis Stack's JSON module) stores JSON documents natively in Redis memory, allowing sub-millisecond reads and writes of individual JSON paths without serializing the entire document. JSON.GET with a JSONPath selector reads a nested field in ~0.1 ms — 50× faster than fetching a serialized JSON string and parsing it in application code; JSON.ARRAPPEND adds array elements atomically without a read-modify-write cycle. This guide covers JSON.SET and JSON.GET commands, JSONPath v2 syntax, atomic array and object mutations, RediSearch indexing over JSON fields, and Node.js integration with the official redis client.

JSON.SET and JSON.GET: Core Read/Write Commands

JSON.SET and JSON.GET are the fundamental commands for writing and reading RedisJSON documents. JSON.SET key path value stores a JSON value at a path inside the document — use $ as the path to write the entire root document, or a JSONPath expression to update a nested field atomically. JSON.GET retrieves a path (or multiple paths in one round-trip) and returns the result as a JSON string.

# Set an entire document at the root ($)
JSON.SET user:1 $ '{"name":"Alice","age":30,"address":{"city":"NYC","zip":"10001"}}'

# Read the entire document
JSON.GET user:1 $
# => [{"name":"Alice","age":30,"address":{"city":"NYC","zip":"10001"}}]

# Read a single nested field
JSON.GET user:1 $.address.city
# => ["NYC"]

# Update a single nested field without touching the rest of the document
JSON.SET user:1 $.address.city '"London"'
JSON.GET user:1 $.address.city
# => ["London"]

# NX flag: set only if key does NOT exist (create-or-skip)
JSON.SET user:2 $ '{"name":"Bob"}' NX

# XX flag: set only if key already EXISTS (update-or-skip)
JSON.SET user:1 $.age 31 XX

# JSON.MGET: read the same path from multiple keys in one round-trip
JSON.MGET user:1 user:2 $.name
# => [["Alice"], ["Bob"]]

# JSON.MSET: atomically set multiple key-path-value triples
JSON.MSET user:3 $ '{"name":"Carol"}' user:4 $ '{"name":"Dave"}'

The path argument uses JSONPath v2 syntax (described in the next section). When the path matches multiple nodes (e.g., a wildcard), JSON.SET updates all matching locations atomically. JSON.GET always returns a JSON array of matched values — even a single match is wrapped in an array because JSONPath can match multiple nodes. The NX and XX conditional flags make JSON.SET behave like the Redis SET NX/SET XX flags, enabling safe concurrent creates and updates without separate existence checks.

JSONPath v2 Syntax for Redis

RedisJSON implements JSONPath v2 (the IETF draft standard). The path always starts with $ (root). Dot notation navigates object keys; bracket notation provides array indexing, wildcards, filters, and union selectors. All paths in RedisJSON commands use this syntax — understanding it unlocks precise partial reads and updates.

# Root: the entire document
JSON.GET order:1 $

# Dot notation: navigate object keys
JSON.GET order:1 $.customer.name

# Recursive descent: find ALL 'price' fields at any depth
JSON.GET order:1 $..price

# Array index: first item (0-based)
JSON.GET order:1 $.items[0]

# Last array element (negative index)
JSON.GET order:1 $.items[-1]

# Wildcard: ALL elements of an array
JSON.GET order:1 $.items[*]

# Wildcard on object: ALL values of an object
JSON.GET order:1 $.metadata.*

# Filter expression: items where price < 100
JSON.GET order:1 '$.items[?(@.price < 100)]'

# Filter with string match: items where category == "electronics"
JSON.GET order:1 '$.items[?(@.category == "electronics")]'

# Union: retrieve name AND price in one path
JSON.GET order:1 '$.items[*]["name","price"]'

# Multiple paths in one JSON.GET call
JSON.GET order:1 $.customer.name $.totalAmount

# Slice: items index 1 through 3 (exclusive)
JSON.GET order:1 '$.items[1:3]'

Filter expressions use @ to refer to the current element being tested. Comparison operators ==, !=, <, <=, >, >= are supported, as are logical && and ||. The recursive descent operator .. is powerful but scans the entire document tree — use it sparingly on large documents in hot paths. For high-frequency reads of specific paths, always prefer explicit dot/bracket paths over recursive descent to minimize the traversal cost.

Atomic JSON Mutations: NUMINCRBY, ARRAPPEND, OBJKEYS

RedisJSON provides specialized mutation commands that operate atomically on individual paths — no read-modify-write cycle needed. These commands are the idiomatic way to update numeric counters, manage array queues, inspect document structure, and remove document paths in production systems.

# JSON.NUMINCRBY: atomically increment a numeric field
JSON.SET product:1 $ '{"name":"Widget","stock":100,"price":9.99}'
JSON.NUMINCRBY product:1 $.stock -1
# => [99]
JSON.NUMINCRBY product:1 $.price 0.50
# => [10.49]

# JSON.NUMMULTBY: multiply a numeric field
JSON.NUMMULTBY product:1 $.price 1.1
# => [11.539]

# JSON.ARRAPPEND: push elements onto an array (atomic)
JSON.SET queue:1 $ '{"tasks":[]}'
JSON.ARRAPPEND queue:1 $.tasks '"task-1"' '"task-2"'
# => [2]  (new array length)

# JSON.ARRPOP: pop an element from an array by index
JSON.ARRPOP queue:1 $.tasks 0
# => ["task-1"]  (removes and returns index 0)
JSON.ARRPOP queue:1 $.tasks -1
# => ["task-2"]  (removes and returns last element)

# JSON.ARRINSERT: insert at a specific index
JSON.ARRINSERT queue:1 $.tasks 0 '"urgent-task"'

# JSON.ARRLEN: get array length without fetching elements
JSON.ARRLEN queue:1 $.tasks
# => [1]

# JSON.ARRINDEX: find first occurrence of a value
JSON.ARRINDEX queue:1 $.tasks '"urgent-task"'
# => [0]

# JSON.OBJKEYS: list all keys of a JSON object
JSON.SET config:1 $ '{"host":"localhost","port":6379,"tls":true}'
JSON.OBJKEYS config:1 $
# => [["host","port","tls"]]

# JSON.OBJLEN: count keys in a JSON object
JSON.OBJLEN config:1 $
# => [3]

# JSON.TYPE: get the JSON type of a path
JSON.TYPE product:1 $.stock
# => ["integer"]
JSON.TYPE product:1 $.name
# => ["string"]
JSON.TYPE queue:1 $.tasks
# => ["array"]

# JSON.DEL: remove a path from the document
JSON.DEL product:1 $.price
# => 1  (number of paths deleted)
JSON.GET product:1 $
# => [{"name":"Widget","stock":99}]

# JSON.STRAPPEND: append to a string field
JSON.STRAPPEND product:1 $.name '" Pro"'
# => [10]  (new string length)

# JSON.STRLEN: get string length
JSON.STRLEN product:1 $.name
# => [10]

JSON.NUMINCRBY is the correct way to implement atomic counters inside JSON documents — it avoids the race condition of JSON.GET + add + JSON.SET. JSON.ARRAPPEND and JSON.ARRPOP together form a persistent JSON queue pattern: producers call ARRAPPEND and consumers call ARRPOP 0 (FIFO). For stack-style access, use ARRPOP -1 (LIFO). All array commands return the new length (or the popped value), making them safe for use in multi-producer/multi-consumer pipelines without additional locking.

RediSearch Indexing Over JSON Documents

RediSearch (bundled in Redis Stack) provides full-text search, structured filtering, and aggregation over RedisJSON documents. Create an index with FT.CREATE ON JSON, map JSONPath expressions to schema field names, and query with FT.SEARCH. Indexing is maintained automatically — any JSON.SET on a key matching the index prefix updates the index in the background.

# FT.CREATE: define a search index over JSON documents
FT.CREATE idx:products
  ON JSON
  PREFIX 1 product:
  SCHEMA
    $.name          AS name         TEXT    WEIGHT 2.0
    $.description   AS description  TEXT
    $.price         AS price        NUMERIC SORTABLE
    $.category      AS category     TAG     SEPARATOR ","
    $.inStock       AS inStock      TAG
    $.location      AS location     GEO

# Insert JSON documents (automatically indexed)
JSON.SET product:1 $ '{"name":"Laptop","description":"Fast laptop","price":999,"category":"electronics","inStock":"true","location":"-73.935242,40.730610"}'
JSON.SET product:2 $ '{"name":"Phone","description":"Smart phone","price":499,"category":"electronics,mobile","inStock":"true","location":"-73.935242,40.730610"}'
JSON.SET product:3 $ '{"name":"Desk","description":"Standing desk","price":350,"category":"furniture","inStock":"false"}'

# FT.SEARCH: full-text search on TEXT fields
FT.SEARCH idx:products "laptop"

# Numeric range filter: price between 0 and 500
FT.SEARCH idx:products "@price:[0 500]"

# TAG filter: exact category match
FT.SEARCH idx:products "@category:{electronics}"

# Multiple TAG values (OR): electronics OR furniture
FT.SEARCH idx:products "@category:{electronics | furniture}"

# Combine TEXT and NUMERIC filters
FT.SEARCH idx:products "@category:{electronics} @price:[0 600]"

# SORTBY: sort results by price ascending
FT.SEARCH idx:products "*" SORTBY price ASC

# RETURN: fetch specific JSON fields in results
FT.SEARCH idx:products "@category:{electronics}" RETURN 2 name price

# LIMIT: pagination
FT.SEARCH idx:products "*" LIMIT 0 10

# GEO filter: products within 10km of a point
FT.SEARCH idx:products "@location:[-73.935242 40.730610 10 km]"

# Drop and recreate an index (does not delete the JSON documents)
FT.DROPINDEX idx:products

The AS field_alias syntax maps a JSONPath to a search field name used in queries. TAG fields support exact-match and multi-value filtering — separate multiple tag values in the JSON document with the configured SEPARATOR character (default comma). NUMERIC SORTABLE fields enable range queries and SORTBY. TEXT fields are tokenized for full-text search with TF-IDF scoring. Index creation is synchronous for small datasets and asynchronous for large key sets — check indexing progress with FT.INFO idx:products and look at the indexing and percent_indexed fields.

Node.js Integration with Official Redis Client

The official redis npm package (v4+) ships with full TypeScript support for RedisJSON commands via the client.json sub-object. Install it, create a client, and call JSON commands directly — no additional plugin or configuration needed. For object-relational mapping, redis-om provides a higher-level API with schema-driven document management.

// Install: npm install redis redis-om
import { createClient } from 'redis'

// Create and connect a client
const client = createClient({ url: 'redis://localhost:6379' })
await client.connect()

// JSON.SET: write a document
await client.json.set('user:1', '$', {
  name: 'Alice',
  age: 30,
  roles: ['admin', 'editor'],
  address: { city: 'NYC', zip: '10001' },
})

// JSON.GET: read the entire document
const user = await client.json.get('user:1')
// TypeScript type: unknown — narrow as needed

// JSON.GET with a path: read a single field
const city = await client.json.get('user:1', { path: '$.address.city' })
// => [ 'NYC' ]

// JSON.SET: update a single nested field atomically
await client.json.set('user:1', '$.address.city', 'London')

// JSON.NUMINCRBY: atomic counter increment
await client.json.numIncrBy('user:1', '$.age', 1)
// => [ 31 ]

// JSON.ARRAPPEND: push onto an array
await client.json.arrAppend('user:1', '$.roles', 'viewer')
// => [ 3 ]  (new array length)

// JSON.ARRPOP: pop from an array
await client.json.arrPop('user:1', '$.roles', -1)
// => [ 'viewer' ]

// JSON.DEL: remove a path
await client.json.del('user:1', '$.address.zip')

// Pipeline: batch multiple JSON commands in one round-trip
const pipeline = client.multi()
pipeline.json.set('user:2', '$', { name: 'Bob', age: 25 })
pipeline.json.numIncrBy('user:1', '$.age', 1)
pipeline.json.arrAppend('user:1', '$.roles', 'superadmin')
const results = await pipeline.exec()
// results: array of responses for each command

// redis-om: TypeScript class-to-RedisJSON mapping
import { Entity, Schema, Repository, Client } from 'redis-om'

class Product extends Entity {}
const productSchema = new Schema(Product, {
  name:     { type: 'string' },
  price:    { type: 'number' },
  category: { type: 'string[]', },
  inStock:  { type: 'boolean' },
})

const omClient = new Client()
await omClient.open('redis://localhost:6379')
const repo = omClient.fetchRepository(productSchema)
await repo.createIndex()

// Save a new entity (auto-generates an ID)
const product = repo.createEntity({ name: 'Widget', price: 9.99, category: ['tools'], inStock: true })
await repo.save(product)

// Find by field value using redis-om query builder
const tools = await repo.search()
  .where('category').contains('tools')
  .and('price').lt(20)
  .return.all()

The client.multi() pipeline queues commands and flushes them in a single TCP round-trip — use it when setting multiple JSON documents or performing a sequence of mutations that should be sent together (though not atomically; for atomicity, use a Lua script or Redis transactions with MULTI/EXEC). For connection pooling in a Node.js application, set the socket.keepAlive option and reuse the single createClient instance across requests — the official redis client is already connection-pooled internally.

JSON Expiry, Persistence, and Key Patterns

RedisJSON keys are standard Redis keys and support all Redis key-level operations: EXPIRE, TTL, PERSIST, RENAME, and DEL. Expiry is set at the key level (not at the path level), and Redis persistence (RDB or AOF) applies to JSON documents the same way it applies to Strings and Hashes.

# Set a JSON document with an expiry (60 seconds)
JSON.SET session:abc123 $ '{"userId":1,"roles":["admin"],"createdAt":"2026-05-19T00:00:00Z"}'
EXPIRE session:abc123 3600

# Set expiry at creation time using PEXPIRE (milliseconds)
JSON.SET rate:user:1 $ '{"count":0,"windowStart":1716076800000}'
PEXPIRE rate:user:1 60000

# Check remaining TTL
TTL session:abc123
# => 3598

# Make a key persistent (remove its TTL)
PERSIST session:abc123

# Key naming conventions for namespaced JSON stores
# Pattern: entity:id
JSON.SET user:1001 $ '{"name":"Alice"}'

# Pattern: entity:id:subresource (for large sub-documents stored separately)
JSON.SET user:1001:preferences $ '{"theme":"dark","language":"en"}'

# Pattern: tenant:tenantId:entity:id (multi-tenant isolation)
JSON.SET acme:user:1001 $ '{"name":"Alice"}'
JSON.SET beta:user:1001 $ '{"name":"Bob"}'

# Scan keys by pattern (use SCAN, not KEYS, in production)
# SCAN 0 MATCH "acme:user:*" COUNT 100

# Redis persistence configuration (redis.conf)
# RDB snapshot (point-in-time): save 900 1 / save 300 10 / save 60 10000
# AOF (append-only log): appendonly yes / appendfsync everysec

# Check persistence status
# INFO persistence

# OBJECT ENCODING: inspect the internal encoding of a JSON key
# OBJECT ENCODING user:1001
# => "ReJSON-RL" (RedisJSON binary tree encoding)

For multi-tenant JSON stores, prefix keys with the tenant identifier (tenantId:entity:id) and use RediSearch index prefixes to scope queries per tenant. Avoid using KEYS * in production — use SCAN with a MATCH pattern and a COUNT hint, which iterates the keyspace incrementally without blocking the server. For session caching with automatic expiry, pair JSON.SET with EXPIRE (or use the EX option in SET for String keys). Note that RedisJSON does not support per-path expiry — expiry is always on the entire key.

Performance Patterns: Caching and Hot-Path Optimization

RedisJSON's primary performance advantage over String-based JSON caching is partial path reads and atomic partial writes — reducing both network bandwidth and CPU-side parsing cost. This section covers benchmarking, partial path reads, RESP3 protocol benefits, and read replica routing for JSON-heavy workloads.

// Benchmark: Redis String GET + JSON.parse vs JSON.GET with path
// Scenario: 10 KB document, reading a 20-byte nested field

// ── String approach (bad for large documents) ──
const raw = await client.get('config:app')           // transfers 10 KB
const config = JSON.parse(raw!)                       // CPU: parse 10 KB
const timeout = config.database.connectionTimeout     // access nested field

// ── RedisJSON approach (partial path read) ──
const timeout = await client.json.get('config:app', {
  path: '$.database.connectionTimeout',               // transfers ~20 bytes
})
// ~50x less bandwidth, no JSON.parse overhead in application code

// Minimize round-trips: read multiple paths in one JSON.GET call
const [name, price, stock] = await Promise.all([
  client.json.get('product:1', { path: '$.name' }),
  client.json.get('product:1', { path: '$.price' }),
  client.json.get('product:1', { path: '$.stock' }),
])
// Better: single JSON.GET with multiple paths (one round-trip)
const result = await client.sendCommand([
  'JSON.GET', 'product:1', '$.name', '$.price', '$.stock'
])

// Pipeline batching: multiple JSON reads in one TCP round-trip
const pipe = client.multi()
pipe.json.get('product:1', { path: '$.price' })
pipe.json.get('product:2', { path: '$.price' })
pipe.json.get('product:3', { path: '$.price' })
const prices = await pipe.exec()

// RESP3 protocol: binary-safe JSON transfers (redis client v4+ default)
// Enabled automatically with: createClient({ socket: { tls: false } })
// RESP3 sends bulk strings as binary — avoids UTF-8 re-encoding overhead

// Read replica routing for JSON GET queries (high-read workloads)
const readClient = createClient({
  url: 'redis://replica.host:6379',  // point to a read replica
})
await readClient.connect()
// Route all JSON.GET calls to readClient, JSON.SET to primary client

// Cache-aside pattern with RedisJSON
async function getProduct(id: string) {
  const cached = await client.json.get(`product:${id}`)
  if (cached) return cached

  const product = await db.products.findById(id)  // database fetch
  await client.json.set(`product:${id}`, '$', product)
  await client.expire(`product:${id}`, 300)        // 5-minute TTL
  return product
}

// Write-through pattern: update DB and cache atomically
async function updateProductPrice(id: string, price: number) {
  await db.products.update({ where: { id }, data: { price } })
  // Partial update: only overwrite the price field in the cached document
  await client.json.numIncrBy(`product:${id}`, '$.price', price - (await db.products.findById(id)).price)
  // Or invalidate and let the next read repopulate:
  await client.del(`product:${id}`)
}

The most impactful optimization for JSON-heavy caches is using partial path reads instead of fetching the full document. In a typical product catalog where each document is 5-20 KB, a price-check endpoint that reads only $.price transfers 20 bytes instead of 15 KB — a 750× reduction in network payload per request. Combine partial reads with pipeline batching to further reduce round-trips. For read-heavy workloads, route JSON.GET commands to Redis read replicas using a separate createClient instance pointed at the replica endpoint, while routing all write commands (JSON.SET, JSON.NUMINCRBY) to the primary.

Key Terms

JSONPath
A standardized query language for navigating JSON documents, analogous to XPath for XML. RedisJSON implements JSONPath v2 (IETF draft). The root is $; dot notation ($.user.name) navigates object keys; bracket notation ([0], [*], [?(@.price < 100)]) handles array indexing, wildcards, and filter expressions; the recursive descent operator .. traverses all descendants. JSONPath is used as the path argument in all RedisJSON commands (JSON.SET, JSON.GET, JSON.DEL, etc.) to target specific nodes within a stored JSON document.
RedisJSON
A Redis module (also called the JSON module) that extends Redis with native JSON document storage. RedisJSON stores documents in a compact binary tree structure, enabling atomic partial reads and writes via JSONPath selectors. It ships as part of Redis Stack (alongside RediSearch, RedisGraph, and RedisTimeSeries) and is available in Redis Cloud. RedisJSON exposes a command set prefixed with JSON.: JSON.SET, JSON.GET, JSON.DEL, JSON.NUMINCRBY, JSON.ARRAPPEND, and others. The module handles type checking, path traversal, and mutation atomically within the Redis event loop.
RediSearch
A Redis module providing full-text search, structured filtering, aggregation, and vector similarity search over Redis data structures — including RedisJSON documents. RediSearch indexes are defined with FT.CREATE, specifying field types (TEXT, TAG, NUMERIC, GEO, VECTOR) and their JSONPath mappings for JSON documents. Indexes are maintained automatically as documents are written. Queries are executed with FT.SEARCH using a query language that combines full-text search syntax with structured filter predicates. RediSearch ships as part of Redis Stack and integrates natively with RedisJSON via the ON JSON index type.
RESP3
Redis Serialization Protocol version 3, introduced in Redis 6. RESP3 adds rich data types (maps, sets, doubles, big numbers, verbatim strings) to the wire protocol, replacing the RESP2 encoding that represented all responses as bulk strings or arrays. For RedisJSON, RESP3 enables binary-safe transfer of JSON payloads without UTF-8 re-encoding overhead in the client layer. The official redis Node.js client (v4+) negotiates RESP3 automatically. RESP3 also adds push types that enable client-side caching and server-side invalidation notifications — features relevant for cache consistency in high-frequency JSON read patterns.
pipeline
A Redis client-side batching mechanism that queues multiple commands and sends them to the server in a single TCP round-trip, collecting all responses together. In the redis Node.js client, create a pipeline with client.multi() (which also wraps in a MULTI/EXEC transaction) or use the lower-level pipeline API. Pipelining reduces latency when executing many small JSON commands — for example, reading prices from 100 product keys in one round-trip instead of 100 sequential JSON.GET calls. Note that pipelining batches commands for efficiency but does not provide atomicity; use MULTI/EXEC for atomic command groups.
keyspace
The flat namespace of all keys stored in a Redis database. Redis has no native namespacing — all keys share a single flat keyspace per logical database (db0 through db15). Namespacing is implemented by convention: prefixing keys with a category and identifier separated by colons (entity:id, tenant:id:entity:id). For RedisJSON workloads, consistent key prefixes enable RediSearch index scoping (the PREFIX clause in FT.CREATE) and efficient key scanning with SCAN MATCH prefix:*. Keyspace notifications (notify-keyspace-events) can publish events when JSON keys are created, modified, or expired — useful for cache invalidation pipelines.

FAQ

What is the difference between storing JSON as a Redis String vs using RedisJSON?

Storing JSON as a Redis String serializes the entire document to a flat string. To update a single nested field you must GET the full string, parse it in application code, mutate the value, re-serialize, and SET the entire string back — a read-modify-write cycle that is not atomic and wastes bandwidth proportional to the document size. RedisJSON stores the document as a native binary tree. JSON.SET with a JSONPath selector updates a single field atomically without touching the rest of the document. JSON.GET with a path returns only the targeted field — for a 10 KB document, reading a 20-byte nested field transfers ~20 bytes instead of 10 KB. RedisJSON also provides specialized atomic commands: JSON.NUMINCRBY for counters, JSON.ARRAPPEND for arrays, and JSON.DEL for path removal — none of which require reading the document first. Memory footprint is 30-50% smaller than an equivalent Redis String with JSON.stringify.

How do I install and enable the Redis JSON module?

RedisJSON ships as part of Redis Stack. The easiest local setup is Docker: docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest — this starts Redis with RedisJSON, RediSearch, and other modules pre-loaded. On macOS with Homebrew: brew tap redis-stack/redis-stack && brew install redis-stack, then redis-stack-server. For production, Redis Cloud (cloud.redis.io) provisions fully managed Redis Stack clusters. To verify the module is loaded, run MODULE LIST in redis-cli — you should see an entry for ReJSON. For self-managed Redis, load the module via loadmodule /path/to/rejson.so in redis.conf, or start the server with redis-server --loadmodule /path/to/rejson.so.

What is JSONPath and how does it work with Redis?

JSONPath is a query language for navigating JSON documents. RedisJSON implements JSONPath v2 (IETF draft). Every path starts with $ (root). Dot notation navigates keys: $.user.name. Bracket notation handles arrays: $.items[0] (first element), $.items[-1] (last), $.items[*] (all). The recursive descent operator .. finds a field at any depth: $..price. Filter expressions select matching elements: $.items[?(@.price < 100)]. Union syntax retrieves multiple fields: $.items[*]["name","price"]. Paths are used as arguments in all JSON commands: JSON.SET user:1 $.address.city "London" updates only the city field; JSON.GET user:1 $.address.city returns only that field. JSON.GET always returns a JSON array of matched values, since a path may match multiple nodes.

How do I search JSON fields in Redis?

Use RediSearch (bundled in Redis Stack) to create an index over JSON documents with FT.CREATE: FT.CREATE idx:products ON JSON PREFIX 1 product: SCHEMA $.name AS name TEXT $.price AS price NUMERIC SORTABLE $.category AS category TAG. Once created, the index auto-indexes all existing keys with the product: prefix and new keys as they are written. Query with FT.SEARCH: FT.SEARCH idx:products "@category:{electronics} @price:[0 500]". The RETURN option fetches specific JSON fields in results. TEXT fields support full-text search, TAG fields support exact-match filtering, NUMERIC fields support range queries and sorting, and GEO fields support proximity searches. Check indexing progress with FT.INFO idx:products.

Can I update a nested JSON field without overwriting the whole document?

Yes — JSON.SET with a JSONPath selector updates only the targeted field. Example: JSON.SET user:1 $.address.city "London" updates only address.city; all other document content is untouched. This is a single atomic operation with no read-modify-write cycle. Specialized mutation commands operate on individual paths atomically: JSON.NUMINCRBY product:1 $.stock -1 decrements stock; JSON.ARRAPPEND queue:1 $.tasks "task-42" pushes an element onto the tasks array; JSON.DEL user:1 $.address.zip removes the zip field; JSON.STRAPPEND product:1 $.name " Pro" appends to a string. None of these commands require fetching the document first.

How do I use RedisJSON with Node.js?

Install the official Redis client: npm install redis. Create a client: const client = createClient(); await client.connect(). Use the json sub-object for JSON commands: await client.json.set('user:1', '$', { name: 'Alice', age: 30 }); const user = await client.json.get('user:1'). The package ships full TypeScript types for all JSON commands. For pipeline batching, use client.multi() to queue commands and call exec() to flush them in one round-trip. For object-relational mapping, use redis-om: define a Schema with field types, create a Repository, and call repo.save(entity) — redis-om maps TypeScript class fields to RedisJSON paths automatically and supports a fluent query builder backed by RediSearch.

How does RedisJSON handle JSON arrays?

RedisJSON provides a full set of atomic array commands. JSON.ARRAPPEND queue:1 $.tasks "task-1" pushes an element onto the end of the array and returns the new length. JSON.ARRPOP queue:1 $.tasks 0 removes and returns the element at index 0 (FIFO); JSON.ARRPOP queue:1 $.tasks -1 removes the last element (LIFO). JSON.ARRINSERT inserts at a specific index. JSON.ARRLEN returns the array length without fetching elements. JSON.ARRINDEX finds the first occurrence of a value. All commands are atomic. For producer-consumer queue patterns, combine JSON.ARRAPPEND (producer pushes) with JSON.ARRPOP 0 (consumer pops FIFO). JSONPath filter expressions like $.items[?(@.qty > 0)] can slice arrays on read without modifying the document.

What are the memory implications of storing JSON in Redis?

RedisJSON stores documents in a compact binary tree using RESP3 binary encoding. Redis team benchmarks show a 30-50% smaller memory footprint compared to storing the same document as a Redis String with JSON.stringify, because the binary tree avoids storing redundant key name strings for each element and uses compact type tags. However, very small documents (under ~100 bytes) may have slightly higher overhead than a plain String due to tree node metadata per field. Monitor memory with INFO memory and inspect individual key sizes with DEBUG OBJECT key (shows serializedlength). Enable Redis persistence (RDB snapshots or AOF) to avoid losing JSON documents on restart. Set EXPIRE on ephemeral keys (sessions, rate-limit windows) to prevent unbounded keyspace growth.

Further reading and primary sources