JMESPath Query Language

May 12, 2026 · View on GitHub

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:

ModeWhen to usePackage
InterpretedExpressions are dynamic, determined at runtimeCorvus.Text.Json.JMESPath
Source generatorExpressions are known at build time, embedded in your projectCorvus.Text.Json.JMESPath.SourceGenerator
CLI code generationExpressions are known ahead of time, generated outside the buildCorvus.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.

BenchmarkExpressionJmesPath.NetRTCGJmesPath.Net AllocRT AllocCG Alloc
SimpleFielda8,905 ns49 ns44 ns45,688 B0 B0 B
SimpleSubexpra.b.c9,411 ns62 ns54 ns46,048 B0 B0 B
SimpleOra || b10,174 ns99 ns87 ns46,080 B0 B0 B
LongStringfoo (1 KB value)2,936 ns42 ns7 ns36,008 B0 B0 B
ChainedFiltera[?b==\1`][?c==`2`]`7,430 ns18 ns12 ns34,952 B0 B0 B
Field5050 chained fields16,372 ns434 ns52 ns51,576 B0 B0 B
Index5050 chained indices15,911 ns426 ns53 ns59,224 B0 B0 B
Pipe5050 chained pipes30,333 ns485 ns102 ns51,576 B0 B0 B
DeepAndsNested &&14,791 ns2,311 ns2,155 ns52,568 B0 B0 B
DeepOrsNested ||14,039 ns163 ns119 ns52,568 B0 B0 B
DeepMatchDeep wildcard match9,904 ns299 ns278 ns51,216 B0 B0 B
DeepNoMatchDeep wildcard miss10,783 ns317 ns267 ns52,320 B0 B0 B
DeepProjection[*].[*].[*]58,225 ns223 ns10 ns122,352 B0 B0 B
MultiListMulti-select list25,152 ns4,491 ns4,835 ns56,632 B136 B136 B
SumArraysum(values(@))27,306 ns4,821 ns4,824 ns57,920 B0 B0 B
NestedSumNested sum(...)61,010 ns4,804 ns4,805 ns111,416 B0 B0 B
AvgArrayavg(values(@))7,761 ns1,022 ns1,011 ns48,233 B272 B272 B
MinArraymin(values(@))14,432 ns2,060 ns2,039 ns48,601 B136 B136 B
MaxArraymax(values(@))7,704 ns2,398 ns2,363 ns48,705 B136 B136 B
MinBymin_by(people, &age)24,220 ns2,275 ns1,991 ns64,760 B0 B0 B
MaxBymax_by(people, &age)24,460 ns2,361 ns2,458 ns64,760 B0 B0 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:

CodeSeverityDescription
JPSG001ErrorExpression file not found in AdditionalFiles
JPSG002ErrorExpression file is empty
JPSG003ErrorCode 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>]
ArgumentRequiredDescription
<expressionFile>YesPath to the .jmespath expression file
--classNameYesName of the generated static class
--namespaceYesNamespace for the generated class
--outputPathNoOutput 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:

ErrorExampleDescription
Syntax errorpeople[*Unterminated bracket or invalid syntax
Type errorabs('string')Function called with wrong argument type
Arity errorsum(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

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

Aggregate functions

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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:

FunctionDescription
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

FeatureCorvus.Text.Json.JMESPathJmesPath.Net
Evaluation modelCompiled delegate tree (cached)Direct interpretation
Code generationSource generator + CLI toolNot available
JSON document modelCorvus.Text.Json (pooled, zero-copy)Newtonsoft.Json
Memory modelPooled (JsonWorkspace, ArrayPool)GC-allocated
Zero-allocation hot pathYes (with workspace)No
Conformance (official suite)100%100%
Supported frameworksnet9.0+, netstandard2.0/2.1net45, net6.0+, netstandard2.0/2.1