Json Path
This recipe demonstrates how to query JSON data using JSONPath (RFC 9535) expressions with the Corvus.Text.Json.JsonPath library. It covers property access, wildcards, recursive descent, slicing, filter expressions with logical operators and functions, zero-allocation evaluation, build-time compiled expressions, and custom function extensions.
The Pattern
JSONPath is a standardised query language for selecting nodes within a JSON document. Given a root document $, expressions navigate objects (.name), arrays ([0], [*], [0:2]), and filter nodes ([?expr]). The Corvus implementation passes 100% of the JSONPath Compliance Test Suite (723/723 tests).
Expressions are compiled to an execution plan on first use and cached per evaluator instance. The evaluator is thread-safe and supports zero-allocation queries via QueryNodes with a caller-provided buffer.
The Data
File: bookstore.json
{
"store": {
"book": [
{"category": "reference", "author": "Sandi Toksvig", "title": "Between the Stops", "price": 8.95},
{"category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99},
{"category": "fiction", "author": "Jane Austen", "title": "Pride and Prejudice", "price": 8.99},
{"category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "price": 22.99}
],
"bicycle": {"color": "red", "price": 399.99}
}
}
Basic Queries
Property access
Navigate to a specific property using dot notation:
evaluator.Query("$.store.bicycle.color", data, workspace);
// ["red"]
Wildcards
Select all elements of an array or object:
evaluator.Query("$.store.book[*].author", data, workspace);
// ["Sandi Toksvig","Evelyn Waugh","Jane Austen","J. R. R. Tolkien"]
Recursive descent
Find matching keys at any depth in the document tree:
evaluator.Query("$..author", data, workspace);
// ["Sandi Toksvig","Evelyn Waugh","Jane Austen","J. R. R. Tolkien"]
Index access
Select array elements by position. Negative indices count from the end:
evaluator.Query("$.store.book[0].title", data, workspace); // ["Between the Stops"]
evaluator.Query("$.store.book[-1].title", data, workspace); // ["The Lord of the Rings"]
Array slicing
Select a contiguous range of elements (start:end):
evaluator.Query("$.store.book[0:2].title", data, workspace);
// ["Between the Stops","Sword of Honour"]
Filter Expressions
Simple comparison
evaluator.Query("$.store.book[?@.price<10].title", data, workspace);
// ["Between the Stops","Pride and Prejudice"]
Logical operators
Combine conditions with && (and) and || (or):
evaluator.Query("$.store.book[?@.price<10 && @.category=='fiction'].title", data, workspace);
// ["Pride and Prejudice"]
Built-in functions
RFC 9535 defines five built-in functions: length, count, value, match, and search.
evaluator.Query("$.store.book[?length(@.title)>15].title", data, workspace);
// ["Between the Stops","Pride and Prejudice","The Lord of the Rings"]
Zero-Allocation QueryNodes
For high-throughput scenarios, QueryNodes avoids the overhead of constructing a result array. It pools its buffer internally via ArrayPool<JsonElement> and returns a disposable JsonPathResult:
using JsonPathResult result = evaluator.QueryNodes("$.store.book[*].title", data);
foreach (JsonElement node in result.Nodes)
{
Console.WriteLine(node);
}
If you already have an existing JsonElement[] array that you want results written into, you can pass it as the initialBuffer parameter. If the result exceeds the buffer, the evaluator transparently rents from ArrayPool<JsonElement>. The JsonPathResult must be disposed to return any rented memory.
Source-Generated Expressions
For maximum performance, compile expressions at build time using the [JsonPathExpression] attribute. Add the expression file as an AdditionalFiles item and reference the source generator package.
File: all-authors.jsonpath
$..author
[JsonPathExpression("all-authors.jsonpath")]
public static partial class AllAuthors;
The generator produces optimised C# code that evaluates the expression without parsing or planning at runtime:
using JsonPathResult result = AllAuthors.QueryNodes(data);
foreach (JsonElement node in result.Nodes)
{
Console.WriteLine(node);
}
Custom Function Extensions
The JSONPath evaluator supports custom function extensions. Implement IJsonPathFunction to define a function with typed parameters and return type. Custom functions participate in RFC 9535 well-typedness checking at parse time.
ceil — ValueType → ValueType
A function that returns the ceiling of a numeric value:
sealed class CeilFunction : IJsonPathFunction
{
private static readonly JsonPathFunctionType[] ParamTypes = [JsonPathFunctionType.ValueType];
public JsonPathFunctionType ReturnType => JsonPathFunctionType.ValueType;
public ReadOnlySpan<JsonPathFunctionType> ParameterTypes => ParamTypes;
public JsonPathFunctionResult Evaluate(ReadOnlySpan<JsonPathFunctionArgument> arguments, JsonWorkspace workspace)
{
JsonElement value = arguments[0].Value;
if (value.ValueKind != JsonValueKind.Number)
return JsonPathFunctionResult.Nothing;
int ceiled = (int)Math.Ceiling(value.GetDouble());
return JsonPathFunctionResult.FromValue(ceiled, workspace);
}
}
Register it on a new evaluator instance:
var evaluator = new JsonPathEvaluator(
new Dictionary<string, IJsonPathFunction> { ["ceil"] = new CeilFunction() });
evaluator.Query("$.store.book[?ceil(@.price)==9].title", data, workspace);
// ["Between the Stops","Pride and Prejudice"]
is_fiction — ValueType → LogicalType
A function that returns a logical (boolean) result for use in filter expressions:
sealed class IsFictionFunction : IJsonPathFunction
{
private static readonly JsonPathFunctionType[] ParamTypes = [JsonPathFunctionType.ValueType];
public JsonPathFunctionType ReturnType => JsonPathFunctionType.LogicalType;
public ReadOnlySpan<JsonPathFunctionType> ParameterTypes => ParamTypes;
public JsonPathFunctionResult Evaluate(ReadOnlySpan<JsonPathFunctionArgument> arguments, JsonWorkspace workspace)
{
JsonElement value = arguments[0].Value;
return JsonPathFunctionResult.FromLogical(
value.ValueKind == JsonValueKind.String && value.ValueEquals("fiction"u8));
}
}
var evaluator = new JsonPathEvaluator(
new Dictionary<string, IJsonPathFunction> { ["is_fiction"] = new IsFictionFunction() });
evaluator.Query("$.store.book[?is_fiction(@.category)].title", data, workspace);
// ["Sword of Honour","Pride and Prejudice","The Lord of the Rings"]
cheapest — NodesType → ValueType
A function that receives a node list (all prices) and returns the minimum value. This demonstrates why NodesType matters — the function needs to see every node to compute the result:
sealed class CheapestFunction : IJsonPathFunction
{
private static readonly JsonPathFunctionType[] ParamTypes = [JsonPathFunctionType.NodesType];
public JsonPathFunctionType ReturnType => JsonPathFunctionType.ValueType;
public ReadOnlySpan<JsonPathFunctionType> ParameterTypes => ParamTypes;
public JsonPathFunctionResult Evaluate(ReadOnlySpan<JsonPathFunctionArgument> arguments, JsonWorkspace workspace)
{
ReadOnlySpan<JsonElement> nodes = arguments[0].Nodes;
double min = double.MaxValue;
JsonElement minElement = default;
foreach (JsonElement node in nodes)
{
if (node.ValueKind == JsonValueKind.Number)
{
double v = node.GetDouble();
if (v < min) { min = v; minElement = node; }
}
}
return min < double.MaxValue
? JsonPathFunctionResult.FromValue(minElement)
: JsonPathFunctionResult.Nothing;
}
}
var evaluator = new JsonPathEvaluator(
new Dictionary<string, IJsonPathFunction> { ["cheapest"] = new CheapestFunction() });
evaluator.Query("$.store.book[?@.price==cheapest($.store.book[*].price)].title", data, workspace);
// ["Between the Stops"]
Running the Example
cd docs/ExampleRecipes/028-JsonPath
dotnet run
Related Patterns
- 025-Jsonata — JSONata expression evaluation and transformation
- 026-JMESPath — JMESPath query language
- 024-JsonLogic — JsonLogic rule evaluation
- 023-JsonPatch — RFC 6902 JSON Patch operations