Module: Redis::Commands::Json

Included in:
Redis::Commands
Defined in:
lib/redis/commands/modules/json.rb

Overview

Commands for the RedisJSON module (built into Redis core since 8.0, or available earlier through the RedisJSON module in Redis Stack).

Values are serialized to JSON text on the client before they are sent, and replies that carry JSON text are parsed back into Ruby objects. The serialized form of these replies is identical under RESP2 and RESP3 (RESP3 has no native JSON type), so the handling here is protocol-independent. Note that some other RedisJSON commands — e.g. JSON.NUMINCRBY — DO return differently shaped replies under RESP3 and will need protocol-aware reshaping once RESP3 is supported at this layer.

Constant Summary collapse

NumincrbyNormalize =

Normalize a JSON.NUMINCRBY reply to a protocol-independent value: an array of numbers for a JSONPath, a single number for a legacy path. Under RESP2 the result arrives as a JSON-encoded string ("[3,4]" / "7"); under RESP3 it arrives as native numbers (an array, even for a legacy path). Both shapes map to the same value.

A legacy path can also match multiple values (e.g. "..a"). RESP2 collapses those to the last changed scalar ("7"), while RESP3 returns the full match array with nil for every non-numeric match ([nil, 4, 7, nil]). To stay RESP2-compatible we take the last non-nil element of that array, not its first.

lambda do |reply, jsonpath|
  return nil if reply.nil?

  reply = ::JSON.parse(reply) if reply.is_a?(String) # RESP2 string form
  if jsonpath
    Array(reply)
  elsif reply.is_a?(Array)
    reply.compact.last # RESP3 wraps even a legacy reply in an array; RESP2 keeps the last changed scalar
  else
    reply
  end
end
TypeNormalize =

Normalize a JSON.TYPE reply to a protocol-independent value: an array of type strings for a JSONPath, a single type string for a legacy path. RESP2 returns ["integer"] / "integer"; RESP3 nests one level further as [["integer"]] / ["integer"]. Both map to the same value.

lambda do |reply, jsonpath|
  return nil if reply.nil?

  if jsonpath
    reply.flatten # RESP2 ["integer"] stays flat; RESP3 [["integer"]] collapses
  elsif reply.is_a?(Array)
    reply.first # RESP3 legacy ["integer"] -> "integer"
  else
    reply # RESP2 legacy "integer"
  end
end

Instance Method Summary collapse

Instance Method Details

#json_arrappend(key, path, *values, raw: false) ⇒ Array<Integer>, Integer

Append one or more values to the array at path in the document stored under key.

By default each value is a Ruby object serialized with JSON.generate; pass raw: true to send already-encoded JSON strings through untouched.

Examples:

redis.json_arrappend("doc", "$.colors", "blue")
  # => [3]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target array

  • values (Array<Object>)

    one or more JSON values to append

  • raw (Boolean) (defaults to: false)

    treat each value as an already-encoded JSON string

Returns:

  • (Array<Integer>, Integer)

    the new array length(s); an Array for a JSONPath, a single Integer for a legacy path (nil for a match that is not an array)



262
263
264
265
# File 'lib/redis/commands/modules/json.rb', line 262

def json_arrappend(key, path, *values, raw: false)
  values = values.map { |value| ::JSON.generate(value) } unless raw
  send_command([:"JSON.ARRAPPEND", key, path, *values])
end

#json_arrindex(key, path, value, start: nil, stop: nil, raw: false) ⇒ Array<Integer>, Integer

Return the index of the first occurrence of a scalar value in the array at path. The optional start (inclusive) and stop (exclusive) bound the search.

Examples:

redis.json_arrindex("doc", "$.colors", "silver")
  # => [1]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target array

  • value (Object)

    the scalar JSON value to search for

  • start (Integer) (defaults to: nil)

    optional inclusive start index

  • stop (Integer) (defaults to: nil)

    optional exclusive stop index

  • raw (Boolean) (defaults to: false)

    treat value as an already-encoded JSON string

Returns:

  • (Array<Integer>, Integer)

    the index/indices of the first match (-1 if not found)



281
282
283
284
285
286
287
288
289
# File 'lib/redis/commands/modules/json.rb', line 281

def json_arrindex(key, path, value, start: nil, stop: nil, raw: false)
  value = ::JSON.generate(value) unless raw
  args = [:"JSON.ARRINDEX", key, path, value]
  unless start.nil? && stop.nil?
    args << Integer(start || 0)
    args << Integer(stop) unless stop.nil?
  end
  send_command(args)
end

#json_arrinsert(key, path, index, *values, raw: false) ⇒ Array<Integer>, Integer

Insert one or more values into the array at path before index (existing elements at and after index shift right; 0 prepends, negative counts from the end).

By default each value is a Ruby object serialized with JSON.generate; pass raw: true to send already-encoded JSON strings through untouched.

Examples:

redis.json_arrinsert("doc", "$.colors", 1, "gold")
  # => [4]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target array

  • index (Integer)

    the position to insert before

  • values (Array<Object>)

    one or more JSON values to insert

  • raw (Boolean) (defaults to: false)

    treat each value as an already-encoded JSON string

Returns:

  • (Array<Integer>, Integer)

    the new array length(s)



307
308
309
310
# File 'lib/redis/commands/modules/json.rb', line 307

def json_arrinsert(key, path, index, *values, raw: false)
  values = values.map { |value| ::JSON.generate(value) } unless raw
  send_command([:"JSON.ARRINSERT", key, path, Integer(index), *values])
end

#json_arrlen(key, path = nil) ⇒ Array<Integer>, ...

Return the length of the array at path. When path is omitted it defaults to the root.

Examples:

redis.json_arrlen("doc", "$.colors")
  # => [2]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root "$")

Returns:

  • (Array<Integer>, Integer, nil)

    the array length(s); nil for a match that is not an array, or when the key/path does not exist



322
323
324
325
326
# File 'lib/redis/commands/modules/json.rb', line 322

def json_arrlen(key, path = nil)
  args = [:"JSON.ARRLEN", key]
  args << path if path
  send_command(args)
end

#json_arrpop(key, path = nil, index = nil, raw: false) ⇒ Array, ...

Remove and return an element from the array at path. When path is omitted it defaults to the root; when index is omitted it defaults to -1 (the last element).

The popped element is returned as parsed JSON (a Ruby object), or as the unparsed JSON string when raw: true.

Examples:

redis.json_arrpop("doc", "$.colors", 0)
  # => ["black"]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath to the target array (defaults to the root "$")

  • index (Integer) (defaults to: nil)

    an optional position to pop from (defaults to -1, the last element)

  • raw (Boolean) (defaults to: false)

    return the unparsed JSON string(s) instead of parsed Ruby objects

Returns:

  • (Array, Object, nil)

    the popped value(s); an Array for a JSONPath, a single value for a legacy path, nil for an empty array or a non-array match

Raises:

  • (ArgumentError)

    if index is given without a path



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/redis/commands/modules/json.rb', line 345

def json_arrpop(key, path = nil, index = nil, raw: false)
  raise ArgumentError, "index requires a path" if !index.nil? && path.nil?

  args = [:"JSON.ARRPOP", key]
  if path
    args << path
    args << Integer(index) unless index.nil?
  end

  send_command(args) do |reply|
    if reply.nil? || raw
      reply
    elsif reply.is_a?(Array)
      reply.map { |value| value.nil? ? nil : ::JSON.parse(value) }
    else
      ::JSON.parse(reply)
    end
  end
end

#json_arrtrim(key, path, start, stop) ⇒ Array<Integer>, Integer

Trim the array at path so it keeps only the inclusive range of elements from start to stop (negative indices count from the end).

Examples:

redis.json_arrtrim("doc", "$.colors", 0, 1)
  # => [2]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target array

  • start (Integer)

    inclusive index of the first element to keep

  • stop (Integer)

    inclusive index of the last element to keep

Returns:

  • (Array<Integer>, Integer)

    the new array length(s)



377
378
379
# File 'lib/redis/commands/modules/json.rb', line 377

def json_arrtrim(key, path, start, stop)
  send_command([:"JSON.ARRTRIM", key, path, Integer(start), Integer(stop)])
end

#json_clear(key, path = nil) ⇒ Integer

Clear the container and numeric value(s) at path: arrays and objects are emptied and numbers are set to 0. Strings, booleans and null are left unchanged. When path is omitted it defaults to the root.

Examples:

redis.json_clear("doc", "$.arr")
  # => 1

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root "$")

Returns:

  • (Integer)

    the number of values cleared



218
219
220
221
222
# File 'lib/redis/commands/modules/json.rb', line 218

def json_clear(key, path = nil)
  args = [:"JSON.CLEAR", key]
  args << path if path
  send_command(args)
end

#json_debug_memory(key, path = nil) ⇒ Array<Integer>, Integer

Report the size in bytes of the JSON value(s) at path (the JSON.DEBUG MEMORY subcommand). When path is omitted it defaults to the root.

Examples:

redis.json_debug_memory("doc")
  # => 264

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root)

Returns:

  • (Array<Integer>, Integer)

    the size(s) in bytes: an Array for a JSONPath, a single integer for a legacy path or when no path is given (0 for a missing key)



515
516
517
518
519
# File 'lib/redis/commands/modules/json.rb', line 515

def json_debug_memory(key, path = nil)
  args = [:"JSON.DEBUG", "MEMORY", key]
  args << path if path
  send_command(args)
end

#json_del(key, path = nil) ⇒ Integer

Delete the value(s) at path in the document stored under key. When path is omitted it defaults to the root, so deleting the root removes the whole key.

Examples:

redis.json_del("doc", "$.a")
  # => 1

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root "$")

Returns:

  • (Integer)

    the number of values deleted



186
187
188
189
190
# File 'lib/redis/commands/modules/json.rb', line 186

def json_del(key, path = nil)
  args = [:"JSON.DEL", key]
  args << path if path
  send_command(args)
end

#json_forget(key, path = nil) ⇒ Integer

Delete the value(s) at path in the document stored under key. Alias of #json_del.

Examples:

redis.json_forget("doc", "$.a")
  # => 1

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root "$")

Returns:

  • (Integer)

    the number of values deleted



201
202
203
204
205
# File 'lib/redis/commands/modules/json.rb', line 201

def json_forget(key, path = nil)
  args = [:"JSON.FORGET", key]
  args << path if path
  send_command(args)
end

#json_get(key, *paths, raw: false) ⇒ Object, ...

Get the JSON value(s) at one or more paths in the document stored under key.

By default the reply is parsed from JSON text into a Ruby object. Pass raw: true to get the unparsed JSON string back instead (no JSON.parse).

Examples:

redis.json_get("doc")
  # => { "a" => 1, "nested" => { "b" => 2 } }

raw JSON string

redis.json_get("doc", raw: true)
  # => '{"a":1,"nested":{"b":2}}'

Parameters:

  • key (String)
  • paths (Array<String>)

    zero or more JSONPath expressions; with none the whole document is returned

  • raw (Boolean) (defaults to: false)

    return the unparsed JSON string instead of a parsed Ruby object

Returns:

  • (Object, String, nil)

    the parsed JSON value (or the raw JSON string when raw is true), or nil if the key does not exist



116
117
118
119
120
121
122
123
124
# File 'lib/redis/commands/modules/json.rb', line 116

def json_get(key, *paths, raw: false)
  send_command([:"JSON.GET", key, *paths]) do |reply|
    if reply.nil? || raw
      reply
    else
      ::JSON.parse(reply)
    end
  end
end

#json_merge(key, path, value, raw: false) ⇒ String

Merge value into the document stored under key at path, following RFC 7396 (JSON Merge Patch): a null value deletes a key, a non-null value creates or updates it, and any value merged into an existing array replaces the whole array. For a key that does not yet exist the path must be the root ("$").

By default value is a Ruby object serialized with JSON.generate; pass raw: true to send an already-encoded JSON string through untouched.

Examples:

redis.json_merge("doc", "$.b", 8)
  # => "OK"

Parameters:

  • key (String)
  • path (String)

    a JSONPath (must be "$" when creating a new key)

  • value (Object)

    a JSON-serializable Ruby object, or a pre-encoded JSON string when raw is true

  • raw (Boolean) (defaults to: false)

    treat value as an already-encoded JSON string and send it as-is

Returns:

  • (String)

    the raw "OK" reply



242
243
244
245
# File 'lib/redis/commands/modules/json.rb', line 242

def json_merge(key, path, value, raw: false)
  value = ::JSON.generate(value) unless raw
  send_command([:"JSON.MERGE", key, path, value])
end

#json_mget(*keys, path, raw: false) ⇒ Array

Get the values at a single path from one or more keys.

Returns one element per key, in order, parsed from JSON text into a Ruby object (or nil when the key or path does not exist). Pass raw: true to get the unparsed JSON strings.

Examples:

redis.json_mget("doc1", "doc2", "$.a")
  # => [[1], [2]]

Parameters:

  • keys (Array<String>)

    one or more keys to read

  • path (String)

    a single JSONPath applied to every key

  • raw (Boolean) (defaults to: false)

    return the unparsed JSON strings instead of parsed Ruby objects

Returns:

  • (Array)

    one value per key (nil for a missing key/path)



165
166
167
168
169
170
171
172
173
174
# File 'lib/redis/commands/modules/json.rb', line 165

def json_mget(*keys, path, raw: false)
  keys.flatten!(1)
  send_command([:"JSON.MGET", *keys, path]) do |reply|
    if reply.nil?
      reply
    else
      reply.map { |value| value.nil? || raw ? value : ::JSON.parse(value) }
    end
  end
end

#json_mset(*args, raw: false) ⇒ String

Set one or more JSON values atomically, one per +key+/+path+/+value+ triplet. Either all of the writes are applied or none are. For a key that does not yet exist the path must be the root ("$").

By default each value is a Ruby object serialized with JSON.generate; pass raw: true to send already-encoded JSON strings through untouched.

Examples:

redis.json_mset("doc1", "$", { "a" => 1 }, "doc2", "$", { "b" => 2 })
  # => "OK"

Parameters:

  • args (Array)

    a flat list of key, path, value triplets

  • raw (Boolean) (defaults to: false)

    treat each value as an already-encoded JSON string

Returns:

  • (String)

    the raw "OK" reply

Raises:

  • (ArgumentError)

    unless args is a non-empty list of complete triplets



141
142
143
144
145
146
147
148
149
150
# File 'lib/redis/commands/modules/json.rb', line 141

def json_mset(*args, raw: false)
  raise ArgumentError, "wrong number of arguments (expected key/path/value triplets)" \
    if args.empty? || !(args.size % 3).zero?

  command = [:"JSON.MSET"]
  args.each_slice(3) do |key, path, value|
    command << key << path << (raw ? value : ::JSON.generate(value))
  end
  send_command(command)
end

#json_numincrby(key, path, number) ⇒ Array<Numeric>, ...

Increment the numeric value(s) at path in the document stored under key by number.

Examples:

redis.json_numincrby("doc", "$.a", 2)
  # => [3]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the numeric value(s)

  • number (Numeric)

    the amount to add

Returns:

  • (Array<Numeric>, Numeric, nil)

    the new value(s): an Array for a JSONPath, a single number for a legacy path; nil (or nil element) for a non-numeric match



392
393
394
395
396
397
# File 'lib/redis/commands/modules/json.rb', line 392

def json_numincrby(key, path, number)
  jsonpath = json_path?(path)
  send_command([:"JSON.NUMINCRBY", key, path, number]) do |reply|
    NumincrbyNormalize.call(reply, jsonpath)
  end
end

#json_objkeys(key, path = nil) ⇒ Array

Return the key names of the JSON object(s) at path. When path is omitted it defaults to the root.

Examples:

redis.json_objkeys("doc", "$.nested")
  # => [["b", "c"]]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root)

Returns:

  • (Array)

    an array of key-name arrays (JSONPath) or a single array of key names (legacy path); nil for a non-object match



430
431
432
433
434
# File 'lib/redis/commands/modules/json.rb', line 430

def json_objkeys(key, path = nil)
  args = [:"JSON.OBJKEYS", key]
  args << path if path
  send_command(args)
end

#json_objlen(key, path = nil) ⇒ Array<Integer>, ...

Return the number of keys in the JSON object(s) at path. When path is omitted it defaults to the root.

Examples:

redis.json_objlen("doc", "$.nested")
  # => [2]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root)

Returns:

  • (Array<Integer>, Integer, nil)

    the key count(s): an Array for a JSONPath, a single integer for a legacy path; nil for a non-object match



447
448
449
450
451
# File 'lib/redis/commands/modules/json.rb', line 447

def json_objlen(key, path = nil)
  args = [:"JSON.OBJLEN", key]
  args << path if path
  send_command(args)
end

#json_set(key, path, value, nx: false, xx: false, raw: false) ⇒ Boolean, String

Set the JSON value at path in the document stored under key.

By default value is a Ruby object that is serialized to JSON text with JSON.generate. Pass raw: true to send an already-encoded JSON string through untouched (no re-serialization), which avoids double-encoding pre-built JSON.

Examples:

redis.json_set("doc", "$", { "a" => 1, "nested" => { "b" => 2 } })
  # => "OK"

pre-encoded JSON

redis.json_set("doc", "$", '{"a":1}', raw: true)
  # => "OK"

conditional set with NX/XX returns a boolean

redis.json_set("doc", "$.a", 2, nx: true)
  # => false

Parameters:

  • key (String)
  • path (String)

    a JSONPath, e.g. "$" for the document root

  • value (Object)

    a JSON-serializable Ruby object, or a pre-encoded JSON string when raw is true

  • nx (Boolean) (defaults to: false)

    only set when the path does not already exist

  • xx (Boolean) (defaults to: false)

    only set when the path already exists

  • raw (Boolean) (defaults to: false)

    treat value as an already-encoded JSON string and send it as-is

Returns:

  • (Boolean, String)

    when nx or xx is given, true on success and false when the condition was not met; otherwise the raw "OK" reply

Raises:

  • (ArgumentError)

    if both nx and xx are given (they are mutually exclusive)



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/redis/commands/modules/json.rb', line 82

def json_set(key, path, value, nx: false, xx: false, raw: false)
  raise ArgumentError, "nx and xx are mutually exclusive" if nx && xx

  value = ::JSON.generate(value) unless raw
  args = [:"JSON.SET", key, path, value]
  args << "NX" if nx
  args << "XX" if xx

  if nx || xx
    send_command(args, &BoolifySet)
  else
    send_command(args)
  end
end

#json_strappend(key, path, value, raw: false) ⇒ Array<Integer>, ...

Append value to the JSON string(s) at path in the document stored under key.

By default value is a Ruby string serialized with JSON.generate; pass raw: true to send an already-encoded JSON string through untouched.

Examples:

redis.json_strappend("doc", "$.a", "bar")
  # => [6]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target string(s)

  • value (String)

    the string to append

  • raw (Boolean) (defaults to: false)

    treat value as an already-encoded JSON string and send it as-is

Returns:

  • (Array<Integer>, Integer, nil)

    the new string length(s): an Array for a JSONPath, a single integer for a legacy path; nil for a non-string match



485
486
487
488
# File 'lib/redis/commands/modules/json.rb', line 485

def json_strappend(key, path, value, raw: false)
  value = ::JSON.generate(value) unless raw
  send_command([:"JSON.STRAPPEND", key, path, value])
end

#json_strlen(key, path = nil) ⇒ Array<Integer>, ...

Return the length of the JSON string(s) at path. When path is omitted it defaults to the root.

Examples:

redis.json_strlen("doc", "$.a")
  # => [3]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root)

Returns:

  • (Array<Integer>, Integer, nil)

    the string length(s): an Array for a JSONPath, a single integer for a legacy path; nil for a non-string match



464
465
466
467
468
# File 'lib/redis/commands/modules/json.rb', line 464

def json_strlen(key, path = nil)
  args = [:"JSON.STRLEN", key]
  args << path if path
  send_command(args)
end

#json_toggle(key, path) ⇒ Array, ...

Toggle the boolean value(s) at path in the document stored under key.

Examples:

redis.json_toggle("doc", "$.flag")
  # => [0]

Parameters:

  • key (String)
  • path (String)

    a JSONPath to the target boolean(s)

Returns:

  • (Array, Object, nil)

    +1+/+0+ for the new true/false value(s): an Array for a JSONPath, a single value for a legacy path; nil for a non-boolean match



500
501
502
# File 'lib/redis/commands/modules/json.rb', line 500

def json_toggle(key, path)
  send_command([:"JSON.TOGGLE", key, path])
end

#json_type(key, path = nil) ⇒ Array<String>, ...

Return the type name of the value(s) at path (e.g. "integer", "string", "object"). When path is omitted it defaults to the root.

Examples:

redis.json_type("doc", "$.a")
  # => ["integer"]

Parameters:

  • key (String)
  • path (String) (defaults to: nil)

    an optional JSONPath (defaults to the root)

Returns:

  • (Array<String>, String, nil)

    the type name(s): an Array for a JSONPath, a single string for a legacy path



410
411
412
413
414
415
416
417
# File 'lib/redis/commands/modules/json.rb', line 410

def json_type(key, path = nil)
  jsonpath = json_path?(path)
  args = [:"JSON.TYPE", key]
  args << path if path
  send_command(args) do |reply|
    TypeNormalize.call(reply, jsonpath)
  end
end