RedisJSON: JSON.SET, JSON.GET, JSONPath, and RediSearch

RedisJSON is a Redis module — included in Redis Stack since Redis 7.0 — that stores, queries, and modifies JSON documents natively, replacing the brittle workaround of serializing JSON into Redis strings with JSON.stringify and deserializing on every read. JSON.SET key $ value stores any JSON value at a JSONPath root atomically; JSON.GET key $.field retrieves a single nested field in O(1); JSON.ARRAPPEND key $.tags value appends to a JSON array without reading the whole document — all without a read-modify-write cycle. This guide covers JSON.SET, JSON.GET, JSON.DEL, JSON.ARRAPPEND, JSON.NUMINCRBY, JSONPath query syntax, indexing with RediSearch, and complete Node.js (node-redis) and Python (redis-py) client examples.

Building a JSON payload to store in Redis? Jsonic's JSON Formatter validates and formats JSON instantly.

Open JSON Formatter

Redis Stack and RedisJSON: installation and setup

Redis Stack bundles 5 modules in a single install: RedisJSON 2.x, RediSearch, RedisGraph, RedisTimeSeries, and RedisBloom. Since Redis 7.0, Redis Stack ships as the official recommended distribution for development. For production, you can install the RedisJSON module separately on Redis 6.x or use a managed provider such as Redis Cloud, which enables the module with a single toggle. There are 3 ways to get Redis Stack running locally in under 2 minutes.

# Option 1: Docker (fastest for development)
docker run -p 6379:6379 redis/redis-stack-server:latest

# Option 2: Homebrew on macOS
brew tap redis-stack/redis-stack
brew install redis-stack
redis-stack-server

# Option 3: Build from source (Linux)
# Download the module .so from https://redis.io/downloads
# Add to redis.conf:
loadmodule /path/to/librejson.so

# Verify RedisJSON is loaded
redis-cli MODULE LIST
# Should include: name "ReJSON" ver 20600 (v2.6.x)

Once the module is loaded, the JSON.* command family becomes available. You can confirm with redis-cli COMMAND INFO JSON.SET. For cloud-hosted Redis, check your provider's module marketplace — most managed Redis services expose RedisJSON as a one-click add-on. The module adds no startup overhead to keys that do not use it; existing string, hash, and list keys are unaffected.

JSON.SET and JSON.GET: store and retrieve documents

JSON.SET key path value is the fundamental write command. The path $ targets the document root; any valid JSONPath expression targets a sub-node. JSON.GET key [path ...] reads one or more paths from a single key. JSON.MGET key [key ...] path reads the same path from up to hundreds of keys in 1 network round-trip — a critical optimization for fan-out reads. The table below summarizes the 4 most-used read/write commands.

CommandComplexityReturns
JSON.SET key $ valueO(M+N) where M = depth, N = doc sizeOK
JSON.GET key $.fieldO(N) where N = sub-tree sizeJSON array of matches
JSON.MGET key1 key2 $.fieldO(M*N) where M = keys, N = sub-treeArray of JSON arrays (one per key)
JSON.DEL key $.fieldO(N) where N = sub-tree sizeInteger (number of paths deleted)
# Store a document at the root ($)
JSON.SET user:1 $ '{"name":"Alice","age":30,"tags":["admin","user"],"score":0}'

# Retrieve the entire document
JSON.GET user:1
# => {"name":"Alice","age":30,"tags":["admin","user"],"score":0}

# Retrieve a single field (always returns a JSON array of matches)
JSON.GET user:1 $.name
# => ["Alice"]

# Retrieve multiple fields in one round-trip
JSON.GET user:1 $.name $.age
# => {"$.name":["Alice"],"$.age":[30]}

# Conditional write: NX = only if key does not exist
JSON.SET user:2 $ '{"name":"Bob","age":25}' NX

# Conditional write: XX = only if key already exists
JSON.SET user:1 $.name '"Alice Smith"' XX

# Retrieve the same path from multiple keys (1 round-trip)
JSON.MGET user:1 user:2 $.name
# => [["Alice Smith"], ["Bob"]]

Note that JSON.GET always returns results wrapped in a JSON array — even when the path matches exactly 1 value. This is by design: a path expression can match multiple nodes (wildcards, recursive descent), so the return type is always an array. Unwrap in your application: result[0] gives the scalar for a single-match path. Use Jsonic's JSON Formatter to inspect and validate nested documents before writing them to Redis.

Atomic mutations: JSON.SET sub-paths, JSON.NUMINCRBY, JSON.ARRAPPEND

The power of RedisJSON over raw strings is atomic sub-document mutation. Instead of GET → deserialize → modify → serialize → SET (4 operations, not atomic), you issue a single command that modifies only the targeted path while leaving the rest of the document untouched. There are 6 mutation commands covering the most common operations: field set, numeric increment, array append, array insert, boolean toggle, and field delete.

# Set (or overwrite) a nested field atomically
JSON.SET user:1 $.address.city '"Austin"'
# Only $.address.city changes; all other fields stay intact

# Increment a numeric field (returns new value as array)
JSON.NUMINCRBY user:1 $.score 10
# => [10]

JSON.NUMINCRBY user:1 $.score -3
# => [7]

# Multiply a numeric field
JSON.NUMMULTBY user:1 $.score 2
# => [14]

# Append one or more elements to a JSON array
JSON.ARRAPPEND user:1 $.tags '"moderator"'
# => [3]  (new array length)

JSON.ARRAPPEND user:1 $.tags '"editor"' '"viewer"'
# => [5]

# Insert at a specific index (0 = prepend)
JSON.ARRINSERT user:1 $.tags 0 '"superuser"'
# => [6]

# Get array length without fetching the whole document
JSON.ARRLEN user:1 $.tags
# => [6]

# Delete a field (returns number of paths deleted)
JSON.DEL user:1 $.address.city
# => 1

# Toggle a boolean field
JSON.SET user:1 $.active 'true'
JSON.TOGGLE user:1 $.active
# => [0]  (false)
JSON.TOGGLE user:1 $.active
# => [1]  (true)

All of these commands are atomic at the Redis level — no other client can observe a partial state mid-operation. For distributed counters, JSON.NUMINCRBY is safer than GET + increment + SET because it eliminates the race condition between the read and write. For session data where you need to append events to a log array, JSON.ARRAPPEND avoids the serialization round-trip entirely. See also the JSON examples guide for common document patterns.

JSONPath syntax in RedisJSON

RedisJSON uses a JSONPath dialect closely aligned with RFC 9535. The $ symbol is the root of the document — every path must start with it in the modern syntax. Dot notation ($.key) selects object fields; bracket notation ($['key']) handles keys with special characters. Array elements use zero-based integer indexes, and -1 selects the last element. The table below lists the 7 core path operators with examples against a sample document.

OperatorMeaningExampleResult
$RootJSON.GET k $Entire document
$.keyObject field$.name["Alice"]
$.a.bNested field$.address.city["Austin"]
$.arr[n]Array element (0-based)$.tags[0]["admin"]
$.arr[*]All array elements$.tags[*]["admin","user",...]
$..*Recursive descent (all values)$..nameAll name fields at any depth
$.arr[?(@.price < 10)]Filter expression$.items[?(@.qty > 0)]Array items where qty > 0
# Setup: a product document with nested objects and arrays
JSON.SET product:1 $ '{
  "name": "Widget",
  "price": 9.99,
  "tags": ["sale", "new"],
  "variants": [
    {"size": "S", "qty": 10},
    {"size": "M", "qty": 0},
    {"size": "L", "qty": 5}
  ]
}'

# Root
JSON.GET product:1 $
# => [{"name":"Widget","price":9.99,...}]

# Nested array element
JSON.GET product:1 $.variants[0].size
# => ["S"]

# Last element shorthand
JSON.GET product:1 $.tags[-1]
# => ["new"]

# All elements of an array
JSON.GET product:1 $.variants[*].qty
# => [10, 0, 5]

# Filter: variants with qty > 0
JSON.GET product:1 '$.variants[?(@.qty > 0)]'
# => [{"size":"S","qty":10},{"size":"L","qty":5}]

# Recursive descent: find all 'size' keys at any depth
JSON.GET product:1 $..size
# => ["S","M","L"]

The legacy path syntax (no leading $, e.g., .name) is still supported for backward compatibility but returns a scalar rather than an array on single-match paths. For all new code, use the $-prefixed syntax — it is more predictable and fully supported in RediSearch index definitions. See the JSONPath cheatsheet for a complete reference of operators and filter expressions.

Index and search RedisJSON documents with RediSearch

RediSearch adds secondary indexing to Redis, enabling full-text search, numeric range queries, tag filtering, and geospatial lookups over RedisJSON documents. Index creation takes 3 steps: declare the key prefix pattern, specify ON JSON, and map JSONPath expressions to typed field aliases in the SCHEMA clause. Once created, the index auto-syncs as you write JSON.SET commands — no manual re-indexing required. Memory overhead for a RediSearch index is roughly 10–20% on top of the raw document storage.

# Create an index over all keys prefixed "user:"
FT.CREATE idx:users
  ON JSON
  PREFIX 1 "user:"
  SCHEMA
    $.name      AS name  TEXT
    $.age       AS age   NUMERIC SORTABLE
    $.tags[*]   AS tags  TAG
    $.score     AS score NUMERIC SORTABLE

# Full-text search
FT.SEARCH idx:users "@name:Alice"

# Numeric range
FT.SEARCH idx:users "@age:[25 35]"

# Tag filter (exact match)
FT.SEARCH idx:users "@tags:{admin}"

# Combined predicate
FT.SEARCH idx:users "@tags:{admin} @age:[20 40]"

# Return only specific JSON paths (not full document)
FT.SEARCH idx:users "@tags:{admin}"
  RETURN 2 name age

# Aggregation: average score per tag
FT.AGGREGATE idx:users "*"
  GROUPBY 1 @tags
  REDUCE AVG 1 @score AS avg_score
  SORTBY 2 @avg_score DESC

# Drop the index (does NOT delete the underlying keys)
FT.DROPINDEX idx:users

$.tags[*] mapped as TAG indexes each array element as a separate tag, enabling @tags:{admin} to match documents where the array contains the string "admin" — this is the canonical pattern for multi-value category filters. FT.AGGREGATE supports GROUP BY, REDUCE COUNT/SUM/AVG/MIN/MAX, and APPLY expressions, providing SQL-like analytics without leaving Redis. For more on JSON querying patterns see the JSONPath cheatsheet.

Node.js client: node-redis and ioredis

The node-redis v4+ client ships with a built-in .json sub-client that wraps all JSON.* commands with typed methods and automatic JavaScript object serialization. ioredis does not have native RedisJSON support but accepts raw commands via client.call(). The following examples use node-redis, which is the recommended choice for new Node.js projects. Both clients support connection pooling, TLS, and Redis Cluster.

import { createClient } from 'redis'

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

// --- JSON.SET ---
await client.json.set('user:1', '$', {
  name: 'Alice',
  age: 30,
  tags: ['admin', 'user'],
  score: 0,
})

// --- JSON.GET (full document) ---
const doc = await client.json.get('user:1')
// => { name: 'Alice', age: 30, tags: ['admin', 'user'], score: 0 }

// --- JSON.GET (single path) ---
const name = await client.json.get('user:1', { path: '$.name' })
// => ['Alice']

// --- JSON.MGET (same path, multiple keys) ---
const names = await client.json.mGet(['user:1', 'user:2'], '$.name')
// => [['Alice'], ['Bob']]

// --- JSON.NUMINCRBY ---
const newScore = await client.json.numIncrBy('user:1', '$.score', 10)
// => [10]

// --- JSON.ARRAPPEND ---
const newLen = await client.json.arrAppend('user:1', '$.tags', 'moderator')
// => [3]

// --- JSON.DEL ---
const deleted = await client.json.del('user:1', '$.age')
// => 1

// --- Conditional set (NX) ---
await client.json.set('user:3', '$', { name: 'Carol' }, { NX: true })

// --- Pipeline: batch multiple JSON commands ---
const pipeline = client.multi()
pipeline.json.set('session:abc', '$', { userId: 1, hits: 0 })
pipeline.json.numIncrBy('session:abc', '$.hits', 1)
await pipeline.exec()

await client.disconnect()

Use client.multi() (pipeline) to batch multiple JSON.* commands in a single round-trip. This is essential when initializing a document and immediately updating it — without pipelining you pay 2 network round-trips. For ioredis, send raw commands: await redis.call('JSON.SET', 'key', '$', JSON.stringify(obj)). See JSON examples for more JavaScript patterns.

Python client: redis-py

redis-py 4.0+ includes built-in RedisJSON support via the .json() method accessor, which returns a JSON command group. All JSON.* commands are available as snake_case methods. The library handles Python dict/list serialization and deserialization automatically — pass native Python objects in, get native Python objects back. The async variant usesredis.asyncio.Redis with identical method signatures, making it fully compatible with FastAPI, aiohttp, and other async frameworks.

import redis

# Synchronous client
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# --- JSON.SET ---
r.json().set('user:1', '$', {
    'name': 'Alice',
    'age': 30,
    'tags': ['admin', 'user'],
    'score': 0,
})

# --- JSON.GET (full document) ---
doc = r.json().get('user:1')
# => {'name': 'Alice', 'age': 30, 'tags': ['admin', 'user'], 'score': 0}

# --- JSON.GET (single path) ---
name = r.json().get('user:1', '$.name')
# => ['Alice']

# --- JSON.MGET ---
names = r.json().mget(['user:1', 'user:2'], '$.name')
# => [['Alice'], ['Bob']]

# --- JSON.NUMINCRBY ---
new_score = r.json().numincrby('user:1', '$.score', 10)
# => [10]

# --- JSON.ARRAPPEND ---
new_len = r.json().arrappend('user:1', '$.tags', 'moderator')
# => [3]

# --- JSON.DEL ---
deleted = r.json().delete('user:1', '$.age')
# => 1

# --- Async usage (FastAPI / asyncio) ---
# import redis.asyncio as aioredis
# r = aioredis.Redis(host='localhost', port=6379, decode_responses=True)
# await r.json().set('user:1', '$', {...})
# doc = await r.json().get('user:1')

# --- Pipeline ---
pipe = r.pipeline()
pipe.json().set('session:abc', '$', {'userId': 1, 'hits': 0})
pipe.json().numincrby('session:abc', '$.hits', 1)
pipe.execute()

Set decode_responses=True on the Redis client so that responses are returned as Python strings rather than bytes — without this flag, JSON strings come back as b'"Alice"' instead of '"Alice"'. For production deployments, configure connection pool size with connection_pool=redis.ConnectionPool(max_connections=20, ...) to avoid exhausting connections under load. For working with JSON data in Python more broadly, see the JSON examples guide and the JSON Server guide for mock API patterns.

Definitions

Redis Stack
The official Redis distribution that bundles the core Redis server with 5 modules: RedisJSON, RediSearch, RedisGraph, RedisTimeSeries, and RedisBloom. Available since Redis 7.0 and distributed as a single Docker image or package.
RedisJSON
A Redis module that adds a native JSON data type and the JSON.* command family, enabling atomic sub-document reads and writes without serialization overhead.
JSONPath
A path expression language for selecting nodes within a JSON document. $ is the root; dot notation selects fields; bracket notation selects array elements; wildcards and recursive descent operators enable bulk selection. See the JSONPath cheatsheet for the full syntax reference.
RediSearch
A Redis module (included in Redis Stack) that adds secondary full-text and structured indexes over Redis hashes and JSON documents. Enables FT.SEARCH and FT.AGGREGATE queries over JSON.* keys.
JSON.SET
The primary RedisJSON write command. Syntax: JSON.SET key path value [NX | XX]. Stores any valid JSON value at the given path atomically. NX skips if the key exists; XX skips if the key is absent.
Atomic operation
An operation that completes as a single indivisible step — no other Redis command can observe an intermediate state. All RedisJSON mutation commands (JSON.SET, JSON.NUMINCRBY, JSON.ARRAPPEND, etc.) are atomic at the Redis server level.

Frequently asked questions

What is RedisJSON and how is it different from storing JSON as a Redis string?

RedisJSON is a Redis module that adds a native JSON data type, allowing you to store, retrieve, and manipulate JSON documents without serializing them to strings. With plain Redis strings you must use GET/SET to fetch and overwrite the entire value on every operation — a read-modify-write cycle that is neither atomic nor efficient. RedisJSON stores documents in an internal tree structure and supports atomic sub-document operations: JSON.GET key $.field reads a single nested field in O(1); JSON.ARRAPPEND key $.tags "value" appends to an array without fetching the whole document; JSON.NUMINCRBY key $.count 1 increments a numeric field atomically. RedisJSON 2.x ships as part of Redis Stack since Redis 7.0 alongside RediSearch, RedisGraph, RedisTimeSeries, and RedisBloom — 1 install gives you all 5 modules. A key operational difference: RedisJSON documents use approximately 3–5× more memory than an equivalent raw string due to the tree structure overhead, so memory planning matters at scale.

How do I store and retrieve a JSON document in Redis?

Use JSON.SET to store and JSON.GET to retrieve. JSON.SET key $ value stores any valid JSON value at the root path ($) of the given key. The 3rd argument is the JSON value as a string. Example: JSON.SET user:1 $ '{"name":"Alice","age":30}'. To retrieve the entire document: JSON.GET user:1. To retrieve a single field: JSON.GET user:1 $.name — returns ["Alice"] (always an array of matches). To retrieve multiple fields in 1 round-trip: JSON.GET user:1 $.name $.age. To retrieve the same path from multiple keys simultaneously, use JSON.MGET user:1 user:2 $.name — this returns 1 array per key and costs 1 network round-trip regardless of how many keys you query. The NX and XX flags on JSON.SET control conditional writes: NX only sets if the key does not exist; XX only sets if the key already exists. Always validate your JSON payload before storing — use Jsonic's JSON Formatter to catch syntax errors before they reach Redis.

How do I update a single field in a RedisJSON document without reading the whole object?

RedisJSON supports atomic in-place mutations using path-targeted commands — no read-modify-write cycle required. To set or overwrite a nested field: JSON.SET user:1 $.address.city '"Austin"' — this modifies only that path and leaves the rest of the document untouched. To increment a number: JSON.NUMINCRBY user:1 $.score 10 — atomically adds 10 to the value at $.score. To append a value to a JSON array: JSON.ARRAPPEND user:1 $.tags '"moderator"' — appends the string "moderator" to the tags array. To insert at a specific index: JSON.ARRINSERT user:1 $.tags 0 '"superuser"' — inserts at position 0. To delete a field: JSON.DEL user:1 $.address.city — removes that path entirely, returning the number of paths deleted. To toggle a boolean: JSON.TOGGLE user:1 $.active — flips true to false and vice versa. All of these are O(1) or O(N) where N is the size of the affected sub-tree, not the whole document, making them far more efficient than GET + modify + SET for large documents.

What is JSONPath syntax in RedisJSON?

RedisJSON uses a JSONPath dialect aligned with RFC 9535. The dollar sign $ represents the root of the document — every path must start with it in the modern syntax. A dot followed by a key name selects an object field: $.name, $.address.city. Square brackets with a zero-based integer select an array element: $.tags[0] is the first element, $.tags[-1] is the last. Wildcards select all children: $.* selects all top-level fields; $.tags[*] selects all elements in the tags array. The recursive descent operator .. searches at any depth: $..name finds every field named "name" regardless of nesting level. Filter expressions select elements matching a condition: $.items[?(@.price < 10)] selects array items whose price field is less than 10. Note that JSON.GET always returns results as a JSON array of matches — even a single-match path like $.name returns ["Alice"], not "Alice". See the JSONPath cheatsheet for the complete operator reference.

How do I search RedisJSON documents with RediSearch?

RediSearch (included in Redis Stack) lets you create secondary indexes over RedisJSON keys and run full-text, numeric, tag, and geo queries. Create an index with FT.CREATE, specifying ON JSON and the field mappings: FT.CREATE idx:users ON JSON PREFIX 1 "user:" SCHEMA $.name AS name TEXT $.age AS age NUMERIC $.tags[*] AS tags TAG. This index covers all keys with the prefix "user:" and exposes 3 queryable fields. Once created, the index auto-updates as you write JSON.SET commands — no manual re-indexing needed. To search: FT.SEARCH idx:users "@name:Alice". Numeric range: FT.SEARCH idx:users "@age:[25 35]". Tag filter: FT.SEARCH idx:users "@tags:{admin}". Combine predicates: FT.SEARCH idx:users "@tags:{admin} @age:[20 40]". FT.AGGREGATE supports GROUP BY, REDUCE (COUNT, SUM, AVG), and APPLY transformations — the equivalent of a SQL GROUP BY pipeline over your JSON documents. Index memory overhead is roughly 10–20% on top of raw document storage.

How do I use RedisJSON with Node.js and Python?

For Node.js, use node-redis v4+ which ships with first-class RedisJSON support via the .json sub-client. Example: await client.json.set("user:1", "$", { name: "Alice" }); const doc = await client.json.get("user:1"). The client serializes and deserializes JavaScript objects automatically. Use client.multi() to pipeline multiple JSON commands in 1 round-trip. For Python, use redis-py 4.0+ which includes built-in JSON support via the .json() accessor: r.json().set("user:1", "$", {"name": "Alice"}); doc = r.json().get("user:1"). Set decode_responses=True on the client so responses come back as strings rather than bytes. Both clients support async operation — use redis.asyncio.Redis in Python for FastAPI/asyncio and the same node-redis API with await in Node.js. When benchmarking, JSON.MGET across 100 keys in 1 call typically completes in under 1 ms on a local Redis instance, versus 100 round-trips for individual GET calls.

Ready to work with RedisJSON?

Use Jsonic to format and validate your JSON payload before storing it in Redis. You can also explore common JSON patterns and the JSON localStorage guide for client-side storage patterns.

Open JSON Formatter