Skip to content

JSONata Expression Evaluation

recipe JSON Schema C# jsonata expression evaluation type required items properties format

This recipe demonstrates how to evaluate JSONata expressions against JSON data using the Corvus.Text.Json.Jsonata library. It covers path navigation, filtering, built-in functions, higher-order functions, object construction, variable and function bindings, safety controls, and build-time expression compilation via the source generator.

The Pattern

JSONata is a lightweight query and transformation language for JSON data, inspired by XPath. Expressions can navigate, filter, aggregate, and reshape JSON documents. The Corvus.Text.Json.Jsonata implementation passes 100% of the official JSONata test suite (1,665/1,665 tests).

Expressions are compiled to delegate trees on first use and cached. The evaluator is thread-safe and designed for high-throughput, zero-allocation evaluation.

The Schema

File: order.json

{
    "title": "Order",
    "type": "object",
    "required": ["customer", "items"],
    "properties": {
        "customer": { "type": "string" },
        "items": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["name", "price", "quantity"],
                "properties": {
                    "name": { "type": "string" },
                    "price": { "type": "number" },
                    "quantity": { "type": "integer", "format": "int32" }
                }
            }
        }
    }
}

Evaluating Expressions

EvaluateToString — the simplest API

For quick evaluations where both input and output are JSON strings:

var evaluator = JsonataEvaluator.Default;

string? result = evaluator.EvaluateToString("customer", orderJson);
// "\"Alice\""

result = evaluator.EvaluateToString("$sum(items.(price * quantity))", orderJson);
// "244.99"

Evaluate with parsed documents

For zero-allocation evaluation, parse the data into a JsonElement and provide a JsonWorkspace:

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

JsonElement result = evaluator.Evaluate(
    "$sum(items.(price * quantity))",
    dataDoc.RootElement,
    workspace);

Path Navigation and Filtering

// Simple property
evaluator.EvaluateToString("items[0].name", orderJson);
// "\"Widget\""

// All names (implicit map)
evaluator.EvaluateToString("items.name", orderJson);
// ["Widget","Gadget","Doohickey"]

// Predicate filter
evaluator.EvaluateToString("items[price > 20].name", orderJson);
// ["Widget","Gadget"]

// Computed expression per item
evaluator.EvaluateToString("""items.(name & ": $" & price * quantity)""", orderJson);
// ["Widget: $100","Gadget: $99.99","Doohickey: $45"]

Built-in Functions

JSONata provides 60+ built-in functions. Some highlights:

Category Functions
Aggregation $sum, $count, $max, $min, $average
String $uppercase, $lowercase, $trim, $substring, $split, $join
Type $type, $number, $string, $boolean, $exists
Math $abs, $floor, $ceil, $round, $power, $sqrt
evaluator.EvaluateToString("$sum(items.price)", orderJson);         // 129.49
evaluator.EvaluateToString("$uppercase(customer)", orderJson);      // "ALICE"
evaluator.EvaluateToString("""$join(items.name, ", ")""", orderJson); // "Widget, Gadget, Doohickey"

Higher-Order Functions

// $map — transform each element
evaluator.EvaluateToString(
    """$map(items, function($v) { $v.name & " x" & $v.quantity })""", orderJson);
// ["Widget x4","Gadget x1","Doohickey x10"]

// $filter — select matching elements
evaluator.EvaluateToString(
    """$filter(items, function($v) { $v.price >= 10 })""", orderJson);

// $reduce — fold to a single value
evaluator.EvaluateToString(
    """$reduce(items, function($acc, $v) { $acc + $v.price * $v.quantity }, 0)""", orderJson);
// 244.99

// $sort — custom ordering
evaluator.EvaluateToString(
    """$sort(items, function($a, $b) { $a.price > $b.price }).name""", orderJson);
// ["Doohickey","Widget","Gadget"]

Object Construction and Transformation

JSONata can build new JSON structures inline:

string transform =
    """
    {
        "orderSummary": customer & "'s order",
        "lineItems": items.{
            "product": name,
            "lineTotal": price * quantity
        },
        "grandTotal": $sum(items.(price * quantity))
    }
    """;

evaluator.EvaluateToString(transform, orderJson);
// {"orderSummary":"Alice's order","lineItems":[{"product":"Widget","lineTotal":100}, ...], "grandTotal":244.99}

Variable Bindings

Pass external values into expressions using JsonataBinding:

var bindings = new Dictionary<string, JsonataBinding>
{
    ["taxRate"] = JsonataBinding.FromValue(0.2),
    ["currency"] = JsonataBinding.FromValue("GBP"),
};

JsonElement result = evaluator.Evaluate(
    """$sum(items.(price * quantity)) * (1 + $taxRate) & " " & $currency""",
    dataDoc.RootElement,
    workspace,
    bindings);
// "293.988 GBP"

JsonataBinding.FromValue() accepts double, string, bool, and JsonElement.

Custom C# Function Bindings

Bind C# functions that can be called from JSONata expressions:

var fnBindings = new Dictionary<string, JsonataBinding>
{
    // Simple double→double function (zero-allocation shorthand)
    ["roundUp"] = JsonataBinding.FromFunction((double v) => Math.Ceiling(v)),

    // Binary double→double function
    ["hypot"] = JsonataBinding.FromFunction((double a, double b) => Math.Sqrt(a * a + b * b)),
};

evaluator.Evaluate("$roundUp($sum(items.(price * quantity)) * 1.2)",
    dataDoc.RootElement, workspace, fnBindings);
// 294

evaluator.Evaluate("$hypot(3, 4)", dataDoc.RootElement, workspace, fnBindings);
// 5

For more complex functions, use the full SequenceFunction delegate:

["customFn"] = JsonataBinding.FromFunction(
    (ReadOnlySpan<Sequence> args, JsonWorkspace ws) =>
    {
        // Full access to Sequence API
        double value = args[0].AsDouble();
        return Sequence.FromDouble(value * 2, ws);
    },
    parameterCount: 1)

Safety Controls

Recursion depth limit

Limit the recursion depth to protect against stack overflows from deeply recursive expressions:

try
{
    evaluator.Evaluate(
        "($f := function($n) { $n <= 1 ? 1 : $n * $f($n - 1) }; $f(10))",
        data, maxDepth: 5);
}
catch (JsonataException ex) when (ex.Code == "U1001")
{
    // Stack overflow protection
}

Execution timeout

Set a time limit for expensive expressions:

try
{
    evaluator.Evaluate(
        "($f := function($n) { $n <= 1 ? 1 : $f($n-1) + $f($n-2) }; $f(100))",
        data, timeLimitMs: 100);
}
catch (JsonataException ex) when (ex.Code == "U1001")
{
    // Timeout protection
}

Source-Generated Expressions

For maximum performance, compile expressions at build time using the [JsonataExpression] attribute. Add the expression file as an AdditionalFiles item and reference the source generator:

File: total-price.jsonata

$sum(items.(price * quantity))
[JsonataExpression("total-price.jsonata")]
public static partial class TotalPrice;

The generator compiles the expression to optimized C# at build time:

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

Running the Example

cd docs/ExampleRecipes/025-JSONata
dotnet run