Redis JSON
Respire.Json provides typed Redis JSON commands using caller-supplied System.Text.Json metadata,
so it needs no reflection and supports Native AOT. JSON is built into Redis Open Source 8 and later;
older deployments need Redis Stack or the RedisJSON module.
dotnet add package Respire.Json
using System.Text.Json.Serialization;
using Respire;
using Respire.Json;
await using var client = await RespireClient.ConnectAsync("localhost:6379");
var json = client.Json;
var key = (RespireKey)"customer:42";
var customer = new Customer("Ada", "ada@example.test");
await json.SetAsync(key, customer, CustomerJsonContext.Default.Customer);
var loaded = await json.GetAsync(key, CustomerJsonContext.Default.Customer);
if (loaded.Found)
Console.WriteLine(loaded.Value?.Name);
[JsonSerializable(typeof(Customer))]
internal partial class CustomerJsonContext : JsonSerializerContext;
internal sealed record Customer(string Name, string Email);
- The package, assembly, and root namespace are all
Respire.Json. - With C# 14 or later,
client.Jsonis an extension property onRespireClientandIRespireClient. It reuses one wrapper per client, does no I/O, and leaves client ownership with you. Key-prefixed views get their own wrapper with the view's prefix. - On earlier C# versions, use
new RespireJsonClient(client).
Paths and response shapes
| Path | Example | Returns |
|---|---|---|
| Legacy (default) | ., RespireJsonPath.Root | One value |
| JSONPath | $, RespireJsonPath.JsonPathRoot | Every match |
GetAsyncreads one path and throwsInvalidOperationExceptionon more than one match. UseGetManyAsyncfor wildcards, orGetJsonAsyncto read several paths as text.RespireJsonGetResult<T>.Foundis false when the key is missing or a JSONPath matches nothing.- A stored JSON
nullreturnsFound = truewith a defaultValue, even for a non-nullable reference type. - With a legacy path, a missing path inside an existing document is a server error. Use a
$path to getFound = falseinstead.
Reading and writing
GetJsonAsyncreturns JSON text.RespireJsonGetOptionsmapsIndent,NewLine,Space, andNoEscapetoINDENT,NEWLINE,SPACE, andNOESCAPE, and takes one or morePaths. Do not mix legacy and$paths.SetAsyncserializes typed values straight to UTF-8.SetJsonAsynctakes pre-serialized JSON as a string or UTF-8 bytes. Both returnfalsewhenRespireJsonSetCondition.NxorXxrejects the write.MultiGetAsyncreturns one entry per key:nullfor a missing key, otherwise the matched values.MultiSetAsyncwrites every entry atomically withJSON.MSET. It serializes everything first, so a serialization failure writes nothing.GetMemoryUsageAsyncrunsJSON.DEBUG MEMORY.
Low-level commands
MergeAsync<T>(key, patch, jsonTypeInfo, path) applies an RFC 7396 merge patch:
- Members serialized as JSON
nullare deleted. Omitting a null member leaves it unchanged, so set null handling deliberately. - Arrays and scalars replace the selected value.
- A null patch replaces the target with JSON
null; it does not delete the key.
RespireJsonClient.Commands exposes generated low-level methods for JSON.GET, JSON.SET,
JSON.MGET, JSON.MSET, JSON.DEL, JSON.FORGET, JSON.CLEAR, array, number, object, string,
type, response, and toggle commands, including Commands.MergeAsync (JSON.MERGE),
Commands.ArrayLengthAsync (JSON.ARRLEN), and Commands.NumberPowerByAsync (JSON.NUMPOWBY).
- They return Redis replies as
RespireResult; dispose each result. ArrayLengthAsynckeeps null entries for JSONPath matches that are not arrays.- Conditional
JSON.SETandJSON.DEBUG MEMORYare available only through the typedSetAsync,SetJsonAsync, andGetMemoryUsageAsync.
Projections
Redis 8.10 scalar projections such as $.items.sum(), sum($.items), and ($.price + 1) return
arrays of computed values. Typed reads unwrap that array like ordinary JSONPath matches. A
projection can yield nothing, and GetManyAsync then returns an empty array. See the
Redis JSONPath reference.
A string path (or implicit conversion) is treated as JSONPath only when it starts with $. For
other cases, choose the response shape explicitly:
| Factory | Decodes as | Use for |
|---|---|---|
RespireJsonPath.Legacy(path) | One value | Legacy paths |
RespireJsonPath.JsonPath(path) | Array of matches | JSONPath |
RespireJsonPath.Projection(path) | Array of matches | Projections not starting with $, such as sum($.items) or ($.price + 1) |
RespireJsonPath.DirectArray(path) | One array value | Collection projections such as $.obj.keys() or $.items.append(9) |
- None of these rewrites or validates the expression. Pick the shape Redis actually returns; a mismatch can fail deserialization or regroup results. The JSON reply alone cannot tell you, since a legacy value can itself be an array.
ResponseShapeexposes the stored choice. Two paths are equal only when text and shape match.- Parenthesize numeric-leading arithmetic, for example
(2 * $.n). Redis reads2 * $.nas a legacy path rooted at field2, which may match nothing.
Collection projections return a direct JSON array with no array-of-matches wrapper. Use
DirectArray with array metadata to read it as one value, even though the expression starts with
$:
using System.Text.Json.Serialization;
using Respire;
using Respire.Json;
await using var client = await RespireClient.ConnectAsync("localhost:6379");
var keys = await client.Json.GetAsync("doc", ProjectionJsonContext.Default.StringArray,
RespireJsonPath.DirectArray("$.obj.keys()"));
[JsonSerializable(typeof(string[]))]
internal partial class ProjectionJsonContext : JsonSerializerContext;
GetManyAsync with the same metadata and path returns one value holding that array. Projection(...)
or a plain string path would treat its elements as separate matches.
Key prefixes, Cluster, and client-side caching
- Known RedisJSON key arguments get the
WithKeyPrefixprefix. - All keys in
JSON.MGETandJSON.MSETmust share a Cluster hash slot. Respire checks this before sending and fails with aCROSSSLOTerror. - RESP2 and RESP3 are both supported.
- With client-side caching,
JSON.GET,JSON.MGET, and other JSON reads can be served locally. Single-key writes invalidate only their key;JSON.MSETinvalidates each key it writes.