Skip to content

JMESPath Query Language

Try the JMESPath Playground — evaluate JMESPath expressions in your browser using the Corvus runtime.

Overview

Corvus.Text.Json.JMESPath implements JMESPath for the Corvus.Text.Json document model — a high-performance query language that evaluates JMESPath expressions against JSON data with compiled delegate trees, pipe fusion, and pooled workspace memory.

JMESPath is a query language for JSON. It supports path navigation, sub-expressions, index access, slicing, list and object projections, flatten, filter expressions, multiselect lists and hashes, pipe expressions, comparisons, and a full set of built-in functions. The Corvus implementation passes all 892 conformance test cases in the official JMESPath Compliance Test Suite (100% conformance). The test suite also contains 21 benchmark-only entries that are implemented as BenchmarkDotNet benchmarks with baseline comparison to JmesPath.Net; these are used for performance measurement, not conformance, and are excluded from the count.

Three evaluation modes are available:

Mode When to use Package
Interpreted Expressions are dynamic, determined at runtime Corvus.Text.Json.JMESPath
Source generator Expressions are known at build time, embedded in your project Corvus.Text.Json.JMESPath.SourceGenerator
CLI code generation Expressions are known ahead of time, generated outside the build Corvus.Json.Cli (the jmespath command)

The source generator and CLI tool produce optimized static C# that eliminates delegate dispatch.

Choosing between JMESPath and JSONata: JMESPath is ideal for querying and extracting data from JSON — projections, filtering, slicing, and reshaping with a concise, standardized syntax. If you need arithmetic, string concatenation, user-defined functions, or complex transformations, see JSONata instead. JMESPath is a pure query language with no side effects and no arithmetic operators.

Requirements: The runtime packages target net9.0, net10.0, netstandard2.0, and netstandard2.1. The source generator is an analyzer package and does not impose additional runtime requirements.

Conformance

JMESPath Runtime Conformance JMESPath CodeGen Conformance

Both the runtime evaluator and the code generation pipeline pass all 892 official JMESPath compliance test cases (100% conformance) on .NET 10.0.

Performance

All benchmarks use BenchmarkDotNet with JmesPath.Net as the baseline. The RT column is the interpreted runtime evaluator; the CG column is the source-generated code. Measurements are from a single-threaded run on .NET 10.0.

Benchmark Expression JmesPath.Net RT CG JmesPath.Net Alloc RT Alloc CG Alloc
SimpleField a 8,905 ns 49 ns 44 ns 45,688 B 0 B 0 B
SimpleSubexpr a.b.c 9,411 ns 62 ns 54 ns 46,048 B 0 B 0 B
SimpleOr a \|\| b 10,174 ns 99 ns 87 ns 46,080 B 0 B 0 B
LongString foo (1 KB value) 2,936 ns 42 ns 7 ns 36,008 B 0 B 0 B
ChainedFilter a[?b==\1`][?c==`2`]` 7,430 ns 18 ns 12 ns 34,952 B 0 B 0 B
Field50 50 chained fields 16,372 ns 434 ns 52 ns 51,576 B 0 B 0 B
Index50 50 chained indices 15,911 ns 426 ns 53 ns 59,224 B 0 B 0 B
Pipe50 50 chained pipes 30,333 ns 485 ns 102 ns 51,576 B 0 B 0 B
DeepAnds Nested && 14,791 ns 2,311 ns 2,155 ns 52,568 B 0 B 0 B
DeepOrs Nested \|\| 14,039 ns 163 ns 119 ns 52,568 B 0 B 0 B
DeepMatch Deep wildcard match 9,904 ns 299 ns 278 ns 51,216 B 0 B 0 B
DeepNoMatch Deep wildcard miss 10,783 ns 317 ns 267 ns 52,320 B 0 B 0 B
DeepProjection [*].[*].[*] 58,225 ns 223 ns 10 ns 122,352 B 0 B 0 B
MultiList Multi-select list 25,152 ns 4,491 ns 4,835 ns 56,632 B 136 B 136 B
SumArray sum(values(@)) 27,306 ns 4,821 ns 4,824 ns 57,920 B 0 B 0 B
NestedSum Nested sum(...) 61,010 ns 4,804 ns 4,805 ns 111,416 B 0 B 0 B
AvgArray avg(values(@)) 7,761 ns 1,022 ns 1,011 ns 48,233 B 272 B 272 B
MinArray min(values(@)) 14,432 ns 2,060 ns 2,039 ns 48,601 B 136 B 136 B
MaxArray max(values(@)) 7,704 ns 2,398 ns 2,363 ns 48,705 B 136 B 136 B
MinBy min_by(people, &age) 24,220 ns 2,275 ns 1,991 ns 64,760 B 0 B 0 B
MaxBy max_by(people, &age) 24,460 ns 2,361 ns 2,458 ns 64,760 B 0 B 0 B

All allocations in the RT and CG columns come from JsonDocumentBuilder results returned from aggregate functions (values(), avg()). Navigation, projection, filtering, and arithmetic benchmarks are zero-allocation. JmesPath.Net allocates 34–122 KB per evaluation because it materialises its own object model from System.Text.Json.

Quick start

Install the packages:

dotnet add package Corvus.Text.Json
dotnet add package Corvus.Text.Json.JMESPath

Simplest approach — cloned result, no workspace management:

using Corvus.Text.Json;
using Corvus.Text.Json.JMESPath;

using var dataDoc = ParsedJsonDocument<JsonElement>.Parse("""
    {
      "locations": [
        {"name": "Seattle", "state": "WA"},
        {"name": "New York", "state": "NY"},
        {"name": "Bellevue", "state": "WA"},
        {"name": "Olympia", "state": "WA"}
      ]
    }
    """);

JsonElement result = JMESPathEvaluator.Default.Search(
    "locations[?state == 'WA'].name | sort(@) | {WashingtonCities: join(', ', @)}",
    dataDoc.RootElement);

Console.WriteLine(result); // {"WashingtonCities":"Bellevue, Olympia, Seattle"}

Search(expression, data) parses the input, evaluates the expression, and returns a cloned result that you own — no workspace management or disposal needed.

Full API — zero-allocation evaluation:

using Corvus.Text.Json;
using Corvus.Text.Json.JMESPath;

// Parse the input data (using statement ensures pooled memory is returned)
using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(
    """
    {
      "people": [
        {"first": "James", "last": "d"},
        {"first": "Jacob", "last": "e"},
        {"first": "Jayden", "last": "f"},
        {"missing": "different"}
      ],
      "foo": {"bar": "baz"}
    }
    """);

// Create a workspace for zero-allocation evaluation
using JsonWorkspace workspace = JsonWorkspace.Create();

// Evaluate a JMESPath expression
JsonElement result = JMESPathEvaluator.Default.Search(
    "people[*].first",
    dataDoc.RootElement,
    workspace);

Console.WriteLine(result); // ["James","Jacob","Jayden"]

The evaluator compiles the expression into a delegate tree on first use and caches it. Subsequent evaluations of the same expression skip compilation entirely. Create one JMESPathEvaluator instance and reuse it — JMESPathEvaluator.Default provides a shared static instance.

The workspace provides pooled memory for the result — zero GC allocation per evaluation for most expressions. The result remains valid until the workspace is disposed or reset.

Evaluation modes

Three evaluation modes are available. All three produce the same results for the same expression; they differ in when compilation happens and what overhead is incurred at evaluation time.

Interpreted (runtime evaluation)

Use JMESPathEvaluator when expressions are determined at runtime:

using JsonWorkspace workspace = JsonWorkspace.Create();
var evaluator = new JMESPathEvaluator();

JsonElement result = evaluator.Search(
    "people[*].first",
    dataDoc.RootElement,
    workspace);

Create one JMESPathEvaluator instance and reuse it — it caches compiled delegate trees per expression string and is thread-safe. For simple cases, JMESPathEvaluator.Default provides a shared static instance.

Source generator (build-time code generation)

When expressions are known at build time, the source generator eliminates all runtime compilation overhead.

1. Install the source generator package:

dotnet add package Corvus.Text.Json.JMESPath.SourceGenerator

2. Create a .jmespath expression file (e.g. Expressions/total-price.jmespath):

sum(items[*].price)

3. Declare a partial class with the [JMESPathExpression] attribute:

using Corvus.Text.Json.JMESPath;

namespace MyApp.Queries;

[JMESPathExpression("total-price.jmespath")]
public static partial class TotalPrice;

4. Include the expression file and packages in your .csproj:

<ItemGroup>
  <AdditionalFiles Include="Expressions\total-price.jmespath" />
</ItemGroup>

<ItemGroup>
  <PackageReference Include="Corvus.Text.Json.JMESPath.SourceGenerator"
                    PrivateAssets="all"
                    ReferenceOutputAssembly="false"
                    OutputItemType="Analyzer" />
  <PackageReference Include="Corvus.Text.Json" />
  <PackageReference Include="Corvus.Text.Json.JMESPath" />
</ItemGroup>

5. Call the generated Evaluate method:

using JsonWorkspace workspace = JsonWorkspace.Create();
JsonElement result = TotalPrice.Evaluate(dataDoc.RootElement, workspace);

The generated method is a static method that directly evaluates the expression without delegate dispatch.

Diagnostic messages:

Code Severity Description
JPSG001 Error Expression file not found in AdditionalFiles
JPSG002 Error Expression file is empty
JPSG003 Error Code generation failed (invalid expression or unsupported feature)

CLI code generation

The corvusjson CLI tool includes a jmespath subcommand for ahead-of-time code generation outside the MSBuild pipeline:

dotnet tool install --global Corvus.Json.Cli
corvusjson jmespath <expressionFile> \
    --className <ClassName> \
    --namespace <Namespace> \
    [--outputPath <output.cs>]
Argument Required Description
<expressionFile> Yes Path to the .jmespath expression file
--className Yes Name of the generated static class
--namespace Yes Namespace for the generated class
--outputPath No Output file path (defaults to <ClassName>.cs)

Example:

corvusjson jmespath expressions/total-price.jmespath \
    --className TotalPrice \
    --namespace MyApp.Queries \
    --outputPath Generated/TotalPrice.cs

This produces a self-contained .cs file with the same static Evaluate method as the source generator. Use the CLI tool when:

  • You need to generate code outside the MSBuild pipeline
  • You want to inspect or modify the generated code before committing it
  • You are integrating with a non-.NET build system

How-to guides

Workspace and memory management

The JsonWorkspace provides pooled memory for evaluation results and intermediate values. Using a caller-managed workspace is the recommended pattern for zero-allocation evaluation.

Single evaluation:

using JsonWorkspace workspace = JsonWorkspace.Create();
JsonElement result = evaluator.Search(expression, data, workspace);
// result is valid until workspace is disposed or reset

Batch evaluation — reset the workspace between iterations:

var evaluator = new JMESPathEvaluator();
using JsonWorkspace workspace = JsonWorkspace.Create();

foreach (var item in items)
{
    workspace.Reset();
    JsonElement result = evaluator.Search(expression, item, workspace);
    ProcessResult(result);
}

This pattern achieves zero GC allocation per evaluation for most expressions. The workspace pools memory internally via ArrayPool<byte> and reuses it across evaluations.

Without a workspace (convenience, allocates per call):

// Omit the workspace — the evaluator creates one internally and clones the result
JsonElement result = evaluator.Search(expression, data);

This overload creates a workspace internally, evaluates the expression, clones the result into a standalone JsonElement, and disposes the workspace. It is convenient for one-off evaluations but allocates on every call.

Error handling

All evaluation errors throw JMESPathException with a descriptive message:

try
{
    JsonElement result = evaluator.Search(expression, data, workspace);
}
catch (JMESPathException ex)
{
    Console.WriteLine($"JMESPath error: {ex.Message}");
}

Common error scenarios:

Error Example Description
Syntax error people[* Unterminated bracket or invalid syntax
Type error abs('string') Function called with wrong argument type
Arity error sum(a, b) Function called with wrong number of arguments
Unknown function $custom(x) Unrecognised function name

Expression reference

JMESPath expressions follow the JMESPath specification. This section summarises the key features with examples.

Identifiers

Access a property by name:

// Expression: a
// Data:       {"a": "foo", "b": "bar", "c": "baz"}
// Result:     "foo"

Quoted identifiers allow any string as a property name:

// Expression: "with space"
// Data:       {"with space": "value"}
// Result:     "value"

Sub-expressions

Chain property access with .:

// Expression: a.b.c.d
// Data:       {"a": {"b": {"c": {"d": "value"}}}}
// Result:     "value"

If any key along the path does not exist, the result is null:

// Expression: a.b.c
// Data:       {"a": {"b": {"notc": "value"}}}
// Result:     null

Index expressions

Access array elements by zero-based index. Negative indices count from the end:

// Expression: [0]
// Data:       ["a", "b", "c", "d", "e", "f"]
// Result:     "a"

// Expression: [-1]
// Data:       ["a", "b", "c", "d", "e", "f"]
// Result:     "f"

Slicing

Extract a sub-array with [start:stop:step]:

// Expression: [0:5]
// Data:       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Result:     [0, 1, 2, 3, 4]

// Expression: [5:10]
// Data:       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Result:     [5, 6, 7, 8, 9]

// Expression: [::2]
// Data:       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Result:     [0, 2, 4, 6, 8]

// Expression: [::-1]
// Data:       [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// Result:     [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Projections

Projections evaluate an expression against each element in a collection.

List projections ([*]) — project over an array:

// Expression: people[*].first
// Data:       {"people": [{"first": "James", "last": "d"},
//                         {"first": "Jacob", "last": "e"},
//                         {"first": "Jayden", "last": "f"},
//                         {"missing": "different"}]}
// Result:     ["James", "Jacob", "Jayden"]

Note: null values (from missing keys) are excluded from projection results.

Object projections (*) — project over an object's values:

// Expression: ops.*.numArgs
// Data:       {"ops": {"functionA": {"numArgs": 2},
//                      "functionB": {"numArgs": 3},
//                      "functionC": {"variadic": true}}}
// Result:     [2, 3]

Flatten projections ([]) — flatten nested arrays by one level and project:

// Expression: reservations[].instances[].state
// Data:       {"reservations": [
//               {"instances": [{"state": "running"}, {"state": "stopped"}]},
//               {"instances": [{"state": "terminated"}, {"state": "running"}]}
//             ]}
// Result:     ["running", "stopped", "terminated", "running"]

Filter expressions

Filter arrays with [? <expression>]:

// Expression: machines[?state == 'running'].name
// Data:       {"machines": [
//               {"name": "a", "state": "running"},
//               {"name": "b", "state": "stopped"},
//               {"name": "c", "state": "running"}
//             ]}
// Result:     ["a", "c"]

Comparison operators: ==, !=, <, <=, >, >=.

Logical operators: &&, ||, !.

Pipe expressions

The pipe operator (|) stops the current projection and passes the full result to the right-hand side:

// Expression: people[*].first | [0]
// Data:       {"people": [{"first": "James"}, {"first": "Jacob"}, {"first": "Jayden"}]}
// Result:     "James"

Without the pipe, [0] would be applied to each element of the projection (always returning the first character of each name). The pipe collapses the projection first, then applies [0] to the resulting array.

Multiselect

Multiselect list — create an array from multiple expressions:

// Expression: people[].[name, state.name]
// Data:       {"people": [
//               {"name": "a", "state": {"name": "WA"}},
//               {"name": "b", "state": {"name": "NY"}},
//               {"name": "c", "state": {"name": "WA"}}
//             ]}
// Result:     [["a","WA"],["b","NY"],["c","WA"]]

Multiselect hash — create an object from multiple expressions:

// Expression: people[*].{Name: name, State: state.name}
// Data:       {"people": [
//               {"name": "a", "state": {"name": "WA"}},
//               {"name": "b", "state": {"name": "NY"}}
//             ]}
// Result:     [{"Name":"a","State":"WA"},{"Name":"b","State":"NY"}]

Literal expressions

Use backtick-quoted JSON for literal values in expressions:

// Expression: people[*].name | contains(@, `James`)
// Data:       {"people": [{"name": "James"}, {"name": "Jacob"}]}
// Result:     true

Current node (@)

The @ token refers to the current node — useful inside functions and filter expressions:

// Expression: people[*].name | sort(@)
// Data:       {"people": [{"name": "b"}, {"name": "a"}, {"name": "c"}]}
// Result:     ["a", "b", "c"]

Built-in functions

The implementation supports the full set of JMESPath built-in functions:

Numeric functions

Function Description
abs(n) Absolute value
ceil(n) Ceiling (round up)
floor(n) Floor (round down)

Aggregate functions

Function Description
avg(array) Average of numeric values
max(array) Maximum value (numbers or strings)
min(array) Minimum value (numbers or strings)
sum(array) Sum of numeric values

String functions

Function Description
contains(subject, search) Test if string contains substring, or array contains element
ends_with(str, suffix) Test if string ends with suffix
join(separator, array) Join array of strings with separator
length(subject) Length of string, array, or object
starts_with(str, prefix) Test if string starts with prefix

Type functions

Function Description
to_array(arg) Convert to array (wraps non-arrays in single-element array)
to_number(arg) Convert to number
to_string(arg) Convert to string
type(arg) Returns the type name ("string", "number", "boolean", "array", "object", "null")

Collection functions

Function Description
keys(obj) Object keys as an array
values(obj) Object values as an array
merge(obj1, obj2, ...) Merge objects (later keys win)
not_null(arg1, arg2, ...) Returns the first non-null argument
reverse(array_or_string) Reverse an array or string
sort(array) Sort an array of numbers or strings

Expression-argument functions

These functions take an expression reference (&expr) as an argument:

Function Description
map(&expr, array) Apply expression to each element
max_by(array, &expr) Maximum element by expression key
min_by(array, &expr) Minimum element by expression key
sort_by(array, &expr) Sort array by expression key

Example:

// Expression: max_by(people, &age).name
// Data:       {"people": [{"name": "a", "age": 20}, {"name": "b", "age": 30}, {"name": "c", "age": 10}]}
// Result:     "b"

Common pitfalls

Always dispose ParsedJsonDocument

ParsedJsonDocument<T> rents memory from ArrayPool<byte>. Forgetting to dispose it leaks pooled memory:

// ❌ BAD — leaks pooled memory
var doc = ParsedJsonDocument<JsonElement>.Parse(json);
var result = evaluator.Search(expression, doc.RootElement, workspace);

// ✅ GOOD — using statement returns memory to the pool
using var doc = ParsedJsonDocument<JsonElement>.Parse(json);
var result = evaluator.Search(expression, doc.RootElement, workspace);

Reset the workspace in loops

Without Reset(), workspace memory grows with each evaluation. The workspace remains valid, but old results consume pooled memory unnecessarily:

// ❌ BAD — workspace grows unboundedly
foreach (var item in items)
{
    var result = evaluator.Search(expression, item, workspace);
    ProcessResult(result);
}

// ✅ GOOD — reset frees previous results
foreach (var item in items)
{
    workspace.Reset();
    var result = evaluator.Search(expression, item, workspace);
    ProcessResult(result);
}

Don't forget AdditionalFiles for the source generator

The source generator reads expression files from AdditionalFiles. Without the MSBuild item, the generator can't find the expression and produces diagnostic JPSG001:

<!-- ❌ Missing — generator produces JPSG001 -->
<ItemGroup>
  <None Include="Expressions\query.jmespath" />
</ItemGroup>

<!-- ✅ Correct -->
<ItemGroup>
  <AdditionalFiles Include="Expressions\query.jmespath" />
</ItemGroup>

Result lifetime is tied to the workspace

When you pass a workspace, the returned JsonElement is backed by that workspace's memory. Using the result after the workspace is disposed or reset produces undefined behavior:

// ❌ BAD — result is invalid after workspace disposal
JsonElement result;
using (JsonWorkspace workspace = JsonWorkspace.Create())
{
    result = evaluator.Search(expression, data, workspace);
}
Console.WriteLine(result.GetRawText()); // undefined behavior

// ✅ GOOD — use result before workspace is disposed
using JsonWorkspace workspace = JsonWorkspace.Create();
JsonElement result = evaluator.Search(expression, data, workspace);
Console.WriteLine(result.GetRawText()); // safe

Comparison with other libraries

Feature Corvus.Text.Json.JMESPath JmesPath.Net
Evaluation model Compiled delegate tree (cached) Direct interpretation
Code generation Source generator + CLI tool Not available
JSON document model Corvus.Text.Json (pooled, zero-copy) Newtonsoft.Json
Memory model Pooled (JsonWorkspace, ArrayPool) GC-allocated
Zero-allocation hot path Yes (with workspace) No
Conformance (official suite) 100% 100%
Supported frameworks net9.0+, netstandard2.0/2.1 net45, net6.0+, netstandard2.0/2.1