Skip to content

Json Path

recipe JSON Schema C# 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