Skip to content
Matthew Adams By Matthew Adams Co-Founder · 3 min read
Introducing Corvus.Text.Json V5: JMESPath - On Average 28× Faster JSON Queries

At endjin, we maintain Corvus.JsonSchema, and in the previous post we looked at JSONata for query and transformation.

Now let's look at JMESPath. It's a simpler, standardised alternative where the performance story is, frankly, quite something.

What is JMESPath?

JMESPath is a query language for JSON. You've likely already used it. It's the expression language behind aws --query, az --query, and jp (the JMESPath CLI). It supports path navigation, projections, filtering, slicing, multiselect, pipe expressions, and built-in functions like sort, sum, min, max, join, and contains.

Unlike JSONata, JMESPath is deliberately limited. There is no arithmetic, no user-defined functions, and no side effects. It's a pure query language with a standardised specification and a cross-implementation conformance test suite.

The Corvus implementation passes all 892 test cases. It achieves 100% conformance.

Quick start

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

This is the simplest approach, with a cloned result and no workspace management:

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

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

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

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

That expression filters an array by state, extracts the name field, sorts alphabetically, and constructs a new object with a joined string. All of that happens in a single pipeline.

Zero-allocation evaluation

For production use with controlled lifetime:

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

using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(jsonData);
using JsonWorkspace workspace = JsonWorkspace.Create();

JsonElement result = JMESPathEvaluator.Default.Search(
    "people[?age > `30`].name",
    dataDoc.RootElement,
    workspace);

The workspace pools all intermediate memory. The expression is compiled and cached on first use. Subsequent calls with the same expression are zero-allocation.

Performance

Here's where JMESPath gets interesting. All benchmarks use JmesPath.Net as the baseline:

Benchmark JmesPath.Net Corvus RT Corvus CG RT Alloc CG Alloc
Simple field (a) 6,446 ns 44 ns 35 ns 0 B 0 B
Sub-expression (a.b.c) 6,659 ns 51 ns 49 ns 0 B 0 B
Long string (1 KB value) 3,874 ns 54 ns 11 ns 0 B 0 B
Chained filter 2,697 ns 18 ns 11 ns 0 B 0 B
50 chained fields 21,692 ns 563 ns 80 ns 0 B 0 B
50 chained pipes 24,274 ns 384 ns 92 ns 0 B 0 B
Deep projection ([*].[*].[*]) 82,276 ns 148 ns 18 ns 0 B 0 B
Nested sum 53,033 ns 3,857 ns 3,846 ns 0 B 0 B
Min by age 18,015 ns 1,591 ns 1,588 ns 0 B 0 B

Across all 21 benchmarks, the geometric mean speedup is approximately 28× for the interpreted runtime. Almost every scenario shows zero allocation. That compares with 3–80 KB per query for JmesPath.Net.

The speedups come from several architectural choices:

  1. No per-expression object model - JmesPath.Net parses expressions into an intermediate AST with per-node allocations. Corvus compiles to delegate trees with pooled workspace memory.
  2. Pipe fusion - consecutive pipe stages are fused into a single pass where possible.
  3. UTF-8 throughout - property comparisons operate on raw UTF-8 bytes, avoiding transcoding.
  4. Source-generator optimisation - the code generator eliminates delegate dispatch entirely, which is why CG times are often even lower.

We quote the geometric mean rather than the maximum because the range is enormous (5× to 556×). Individual results depend heavily on the expression pattern. Aggregate functions (sum, min, max) show smaller speedups because both implementations spend most of their time doing arithmetic; simple navigation and projection show the largest gains because the overhead of the expression model dominates in JmesPath.Net.

Source generator

When expressions are known at build time:

using Corvus.Text.Json.JMESPath;

namespace MyApp.Expressions;

[JMESPathExpression("Expressions/active-people.jmespath")]
public static partial class ActivePeopleExpression;

The expression file is registered as an AdditionalFiles item in your project.

CLI code generation

For expressions managed outside the build pipeline:

corvusjson jmespath Expressions/active-people.jmespath \
    --className ActivePeopleQuery \
    --namespace MyApp.Queries \
    --outputPath Generated/

This reads the expression file, generates an optimized static C# class, and writes it to the output path. The generated code is identical to what the source generator produces.

When to choose JMESPath vs JSONata

JMESPath JSONata
Complexity Simple, standardised Expressive, Turing-complete
Arithmetic No Yes
String manipulation join, reverse, sort Full: $substring, $replace, $split, regex
User-defined functions No Yes
Ecosystem AWS CLI, Azure CLI, jp Node-RED, IBM Cloud Pak
Best for Querying and extracting Transforming and reshaping

If your team already uses JMESPath through the AWS or Azure CLI, it's the natural choice for JSON querying in your .NET code. If you need more expressive power, JSONata is the right tool. That includes arithmetic, string manipulation, and custom functions.

Next up

In the next post, we'll complete the query language trilogy with JsonLogic - a safe, side-effect-free rule engine for evaluating business rules stored as JSON.

FAQs

What is JMESPath? JMESPath is a standardised query language for JSON, used by AWS CLI, Azure CLI, and many other tools. It supports path navigation, projections, filtering, slicing, and built-in functions like sort, sum, min, max, and join.
How does Corvus JMESPath compare to JmesPath.Net? On average 28× faster across 21 benchmarks (geometric mean), with zero allocation in most scenarios. JmesPath.Net typically allocates 3–80 KB per query.
When should I use JMESPath instead of JSONata? JMESPath is simpler and standardised - best for querying and extracting data. JSONata is more expressive - it adds arithmetic, string manipulation, user-defined functions, and Turing-complete transformations. If your team already uses JMESPath (via AWS/Azure CLI), it's the natural choice.

Matthew Adams

Co-Founder

Matthew Adams

Matthew was CTO of a venture-backed technology start-up in the UK & US for 10 years, and is now the co-founder of endjin, which provides technology strategy, experience and development services to its customers who are seeking to take advantage of Microsoft Azure and the Cloud.