Skip to content

JsonLogic Rule Engine

Overview

Corvus.Text.Json.JsonLogic implements JsonLogic for the Corvus.Text.Json document model — a safe, side-effect-free rule engine that evaluates JSON-encoded logic rules against JSON data.

JsonLogic is a standard for expressing business rules as JSON. Rules are portable, storable in databases, and safely evaluated without allowing arbitrary code execution. The Corvus implementation passes the full official test suite and adds support for extended numeric types (BigNumber) via custom operators.

Choosing between JsonLogic and JSONata: JsonLogic is ideal for declarative business rules — branching logic, predicates, and simple calculations expressed as JSON. If you need to query, reshape, or transform JSON data (path navigation, filtering, object construction), see JSONata instead.

Try it now: The JSON Logic Playground lets you build rules visually and evaluate them against live data — no setup required.

Three evaluation modes are available:

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

The source generator and CLI tool produce optimized static C# that eliminates delegate dispatch and can constant-fold literal expressions. Benchmarks show generated code is typically 70–98% faster than JsonEverything across 19 scenarios, with zero or near-zero allocations (see benchmark summary).

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.

Quick start

Install the packages:

dotnet add package Corvus.Text.Json
dotnet add package Corvus.Text.Json.JsonLogic

Simplest approach — string in, string out:

using Corvus.Text.Json.JsonLogic;

string? result = JsonLogicEvaluator.Default.EvaluateToString(
    """{"+":[{"var":"a"},{"var":"b"}]}""",
    """{"a":3,"b":4}""");

Console.WriteLine(result); // "7"

EvaluateToString parses the rule and data, evaluates the rule, and returns the result as a JSON string. It is the simplest way to get started — no document parsing, workspace management, or disposal needed.

Full API — zero-allocation evaluation:

using Corvus.Text.Json;
using Corvus.Text.Json.JsonLogic;

// Parse the rule and data (using statements ensure pooled memory is returned)
using var ruleDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"+":[{"var":"a"},{"var":"b"}]}""");
using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"a":3,"b":4}""");

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

// Evaluate
JsonLogicRule rule = new(ruleDoc.RootElement);
JsonElement result = JsonLogicEvaluator.Default.Evaluate(
    rule, dataDoc.RootElement, workspace);

Console.WriteLine(result.GetRawText()); // "7"

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

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

Installation

Interpreted mode (runtime evaluation)

dotnet add package Corvus.Text.Json
dotnet add package Corvus.Text.Json.JsonLogic

Source generator (build-time code generation)

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

The source generator package is a development dependency — it runs at build time and produces C# source; the generated code depends on Corvus.Text.Json and Corvus.Text.Json.JsonLogic at runtime.

CLI tool

dotnet tool install --global Corvus.Json.Cli

The corvusjson tool includes a jsonlogic subcommand. See CLI code generation below.

Interpreted evaluation

Basic evaluation

Parse a rule, wrap it in a JsonLogicRule, and evaluate it against data:

using Corvus.Text.Json;
using Corvus.Text.Json.JsonLogic;

// Parse the rule and data
using var ruleDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"+":[{"var":"a"},{"var":"b"}]}""");
using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"a":3,"b":4}""");

// Evaluate
JsonLogicRule rule = new(ruleDoc.RootElement);
JsonElement result = JsonLogicEvaluator.Default.Evaluate(rule, dataDoc.RootElement);

// result is a number element with value 7
Console.WriteLine(result.GetRawText()); // "7"

When you omit the workspace, the evaluator creates one internally and returns a cloned, self-contained result.

Caller-managed workspace

For zero-allocation evaluation on the hot path, provide your own JsonWorkspace:

using JsonWorkspace workspace = JsonWorkspace.Create();

JsonElement result = JsonLogicEvaluator.Default.Evaluate(
    rule, dataDoc.RootElement, workspace);

When you provide a workspace, the result element is backed by the workspace's memory. It remains valid until the workspace is disposed or reset.

Source generator

For rules known at build time, the source generator eliminates all runtime compilation overhead. Declare a partial class annotated with [JsonLogicRule], pointing at a JSON rule file:

using Corvus.Text.Json.JsonLogic;

namespace MyApp.Rules;

[JsonLogicRule("Rules/discount-rule.json")]
internal static partial class DiscountRule
{
}

Include the JSON rule file as an AdditionalFiles item in your .csproj:

<ItemGroup>
  <AdditionalFiles Include="Rules\discount-rule.json" />
</ItemGroup>

And reference the source generator:

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

At build time, the generator reads discount-rule.json and emits an Evaluate method on the partial class:

using Corvus.Text.Json;
using Corvus.Text.Json.JsonLogic;

using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"price":100,"memberLevel":"gold"}""");

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

The generated Evaluate method is a static method that directly evaluates the rule without delegate dispatch. Literal sub-expressions are constant-folded at build time using BigNumber arithmetic.

Diagnostic messages

Code Severity Description
JLSG001 Error Rule file not found in AdditionalFiles
JLSG002 Error Rule file is empty
JLSG003 Error Code generation failed (invalid JSON or unsupported operator)

CLI code generation

The corvusjson CLI tool includes a jsonlogic subcommand for ahead-of-time code generation:

corvusjson jsonlogic <ruleFile> \
    --className <ClassName> \
    --namespace <Namespace> \
    [--outputPath <output.cs>]
Argument Required Description
<ruleFile> Yes Path to the JSON rule 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 jsonlogic rules/pricing.json \
    --className PricingRule \
    --namespace MyApp.Rules \
    --outputPath Generated/PricingRule.cs

This produces a self-contained .cs file with a static Evaluate method, identical to what the source generator produces. 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

Supported operators

The implementation supports all standard JsonLogic operators:

Data access

Operator Description Example
var Access a data value by path {"var":"user.name"}
missing Return array of missing keys {"missing":["a","b"]}
missing_some Require at least N of the listed keys {"missing_some":[1,["a","b","c"]]}

Logic

Operator Description Example
if / ?: Conditional (if/then/else chain) {"if":[cond, then, else]}
and Short-circuit logical AND {"and":[true, {"var":"x"}]}
or Short-circuit logical OR {"or":[false, {"var":"x"}]}
! Logical NOT {"!":[true]}
!! Double NOT (coerce to boolean) {"!!":[""]}

Comparison

Operator Description Example
== Loose equality (with type coercion) {"==":[1,"1"]}
=== Strict equality (no coercion) {"===":[1,1]}
!= Loose inequality {"!=":[1,2]}
!== Strict inequality {"!==":[1,"1"]}
< Less than (supports 3-arg between) {"<":[1,2,3]}
<= Less than or equal {"<=":[1,1]}
> Greater than {">":[2,1]}
>= Greater than or equal {">=":[2,2]}

Arithmetic

Operator Description Example
+ Addition (n-ary) {"+":[1,2,3]}
- Subtraction or unary negation {"-":[10,3]}
* Multiplication (n-ary) {"*":[2,3,4]}
/ Division {"/":[10,3]}
% Modulo {"%":[10,3]}
min Minimum of values {"min":[1,2,3]}
max Maximum of values {"max":[1,2,3]}

String

Operator Description Example
cat Concatenation {"cat":["hello"," ","world"]}
in Substring test or array membership {"in":["a","abc"]}
substr Substring extraction {"substr":["hello",1,3]}

Array

Operator Description Example
filter Filter array by predicate {"filter":[data, test]}
map Transform each element {"map":[data, transform]}
reduce Fold array to single value {"reduce":[data, reducer, initial]}
merge Flatten/concatenate arrays {"merge":[[1],[2],[3]]}
all All elements satisfy predicate {"all":[data, test]}
some At least one element matches {"some":[data, test]}
none No elements match {"none":[data, test]}

Miscellaneous

Operator Description Example
log Pass-through (for debugging) {"log":{"var":"x"}}

Extended operators

The Corvus implementation adds operators for explicit numeric type conversion, useful when working with BigNumber precision:

Operator Description
asDouble Coerce a value to a double-precision number
asLong Coerce a value to a 64-bit integer
asBigNumber Coerce a value to arbitrary-precision BigNumber
asBigInteger Coerce a value to arbitrary-precision BigInteger (truncated)

These operators are not part of the standard JsonLogic specification but are safe to use with any evaluation mode (interpreted, source generator, or CLI).

Runtime custom operators (IOperatorCompiler)

The interpreted evaluator supports user-defined operators at runtime via the IOperatorCompiler interface. This lets you extend the operator set — or override built-in operators — without code generation.

Overview

When the evaluator compiles a rule, it walks the JSON tree and produces a delegate tree. For each operator it encounters, it looks up the operator name in a dispatch table. Custom operators are checked before the built-in operators, so you can override standard behaviour such as +, var, or if.

Implementing an operator

Implement IOperatorCompiler. The Compile method receives the pre-compiled operand delegates and returns a single RuleEvaluator delegate that applies the operator:

using Corvus.Text.Json.JsonLogic;

public sealed class DoubleItCompiler : IOperatorCompiler
{
    public RuleEvaluator Compile(RuleEvaluator[] operands)
    {
        // Capture the single operand
        RuleEvaluator operand = operands[0];

        return (in JsonElement data, JsonWorkspace workspace) =>
        {
            EvalResult val = operand(data, workspace);
            if (val.TryGetDouble(out double d))
            {
                return EvalResult.FromDouble(d * 2);
            }

            return EvalResult.FromDouble(0);
        };
    }
}

Registering operators

Pass a dictionary of operator name → compiler to the JsonLogicEvaluator constructor:

var customOps = new Dictionary<string, IOperatorCompiler>
{
    ["double_it"] = new DoubleItCompiler(),
};

JsonLogicEvaluator evaluator = new(customOps);

Then use the evaluator as normal. Rules can reference the custom operator by name:

using var ruleDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"double_it":[{"var":"x"}]}""");
using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(
    """{"x":5}""");

JsonLogicRule rule = new(ruleDoc.RootElement);
JsonElement result = evaluator.Evaluate(rule, dataDoc.RootElement);
// result = 10

Custom operators compose freely with built-in operators:

// {"+":[{"double_it":[{"var":"x"}]}, {"var":"y"}]}
// → double_it(3) + 4 = 6 + 4 = 10

Overriding built-in operators

Because custom operators are dispatched first, you can replace any standard operator:

var customOps = new Dictionary<string, IOperatorCompiler>
{
    ["+"] = new AlwaysFortyTwoCompiler(),
};

JsonLogicEvaluator evaluator = new(customOps);
// All "+" rules now return 42, regardless of operands

Key types

Type Description
IOperatorCompiler Interface to implement. Single method: RuleEvaluator Compile(RuleEvaluator[] operands)
RuleEvaluator Delegate: EvalResult RuleEvaluator(in JsonElement data, JsonWorkspace workspace)
EvalResult Discriminated union — holds either a native double or a JsonElement
JsonLogicEvaluator Evaluator. Use the constructor overload accepting IReadOnlyDictionary<string, IOperatorCompiler>

Working with EvalResult

EvalResult is a dual-track value type that avoids the double → JSON → double round-trip for arithmetic:

Method Description
EvalResult.FromDouble(double) Create a result wrapping a native double
EvalResult.FromElement(in JsonElement) Create a result wrapping a JSON element
TryGetDouble(out double) Attempt to extract a double (coercing from JSON number/bool/string if needed)
AsElement(JsonWorkspace) Materialize the result as a JsonElement (writing the double to the workspace if necessary)
IsTruthy() JsonLogic truthiness test
IsNullOrUndefined() True for JSON null or Undefined
ValueKind JsonValueKind.Number for doubles; the element's kind otherwise

When implementing operators that do arithmetic, prefer the double fast path and fall back to BigNumber for arbitrary-precision values. The following example shows how the built-in + operator is implemented internally — custom operators can follow the same pattern:

using Corvus.Numerics;

public sealed class AddCompiler : IOperatorCompiler
{
    public RuleEvaluator Compile(RuleEvaluator[] operands)
    {
        if (operands.Length < 2)
        {
            // Too few arguments — return zero (see error handling below)
            return static (in JsonElement data, JsonWorkspace workspace) =>
                EvalResult.FromDouble(0);
        }

        RuleEvaluator left = operands[0];
        RuleEvaluator right = operands[1];

        return (in JsonElement data, JsonWorkspace workspace) =>
        {
            EvalResult l = left(data, workspace);
            EvalResult r = right(data, workspace);

            // Fast path: both operands fit in a double
            if (l.TryGetDouble(out double a) && r.TryGetDouble(out double b))
            {
                return EvalResult.FromDouble(a + b);
            }

            // Slow path: fall back to BigNumber for full precision
            BigNumber lb = CoerceToBigNumber(l.AsElement(workspace));
            BigNumber rb = CoerceToBigNumber(r.AsElement(workspace));
            return EvalResult.FromElement(
                BigNumberToElement(lb + rb, workspace));
        };
    }

    private static BigNumber CoerceToBigNumber(in JsonElement element)
    {
        if (element.ValueKind == JsonValueKind.Number)
        {
            using var raw = JsonMarshal.GetRawUtf8Value(element);
            if (BigNumber.TryParse(raw.Span, out BigNumber result))
            {
                return result;
            }
        }

        if (element.ValueKind == JsonValueKind.True) return BigNumber.One;
        if (element.ValueKind == JsonValueKind.False
            || element.ValueKind == JsonValueKind.Null) return BigNumber.Zero;

        return BigNumber.Zero;
    }

    private static JsonElement BigNumberToElement(
        BigNumber value, JsonWorkspace workspace)
    {
        Span<byte> buffer = stackalloc byte[64];
        if (value.TryFormat(buffer, out int bytesWritten))
        {
            return JsonLogicHelpers.NumberFromSpan(
                buffer.Slice(0, bytesWritten), workspace);
        }

        return JsonLogicHelpers.Zero();
    }
}

Error handling

JsonLogic follows a philosophy of graceful degradation — operators never throw exceptions at evaluation time. All built-in operators in the Corvus implementation follow this convention, and custom operators should do the same:

Situation Convention Example
Too few arguments Return null or 0 {"/":[]}null
Wrong type (non-numeric for arithmetic) Coerce if possible, fall back to 0 {"+":[true, "3"]}4
Division / modulo by zero Return null {"/":[1, 0]}null
Missing or undefined data Return null {"var":"no.such.path"}null

Use JsonLogicHelpers.NullElement() to return a JSON null, and JsonLogicHelpers.Zero() to return the number 0. Both are allocation-free.

// Too few arguments — return null
if (operands.Length < 2)
{
    return static (in JsonElement data, JsonWorkspace workspace) =>
        EvalResult.FromElement(JsonLogicHelpers.NullElement());
}

// Non-numeric operand — coerce or fall back to zero
if (!val.TryGetDouble(out double d))
{
    d = 0;
}

This means rules with mistakes produce deterministic fallback values rather than aborting evaluation. If your use case requires strict validation, you can throw from Compile (which runs once at compilation time) to reject rules with a known-wrong number of operands — but the evaluation delegate itself should not throw.

When your operator produces a non-numeric result, use AsElement to get operand values and FromElement to return a JsonElement:

return (in JsonElement data, JsonWorkspace workspace) =>
{
    JsonElement value = operands[0](data, workspace).AsElement(workspace);
    // ... produce a JsonElement result ...
    return EvalResult.FromElement(result);
};

Zero-argument operators

Operators that take no arguments are supported — the operands array will be empty:

public sealed class PiCompiler : IOperatorCompiler
{
    public RuleEvaluator Compile(RuleEvaluator[] operands)
    {
        return static (in JsonElement data, JsonWorkspace workspace) =>
            EvalResult.FromDouble(3.14159265358979);
    }
}

Rule: {"pi":[]} — the empty array is required by the JsonLogic format.

Caching

Compiled rules are cached by the evaluator. A custom operator's Compile method is called once per unique rule text, not once per evaluation. The returned RuleEvaluator delegate is invoked on every evaluation. This means:

  • Expensive setup in Compile is acceptable — it runs at compilation time.
  • The returned delegate should be allocation-free on the hot path.
  • Any data captured by the delegate's closure — including the operands array and any values derived from it — is also cached for the lifetime of the evaluator (or until ClearCache() is called). This is by design: the operand delegates form the compiled expression tree and are reused across evaluations with different data.

Custom operator templates (.jlops)

The code generator and source generator support user-defined operators via .jlops template files. Custom operators extend the standard operator set with C# expression or block bodies that are emitted directly into the generated code.

Format

A .jlops file contains one or more operator definitions. Two forms are supported:

Expression form (single line):

op discount(price, percent) => BigNumberToElement(CoerceToBigNumber(price) * (BigNumber.One - CoerceToBigNumber(percent) / (BigNumber)100), workspace);

Block form (multi-line):

op clamp(value, lo, hi)
{
    BigNumber v = CoerceToBigNumber(value);
    BigNumber low = CoerceToBigNumber(lo);
    BigNumber high = CoerceToBigNumber(hi);
    BigNumber clamped = v < low ? low : v > high ? high : v;
    return BigNumberToElement(clamped, workspace);
}

Lines starting with // are comments. Blank lines are ignored.

Each parameter receives a JsonElement value at runtime. The implicit workspace parameter is always available. The body has access to all helper methods in the generated class, including CoerceToBigNumber(), BigNumberToElement(), and JsonLogicHelpers.*.

Using with the source generator

Include .jlops files as AdditionalFiles alongside your JSON rule files:

<ItemGroup>
  <AdditionalFiles Include="Rules\*.json" />
  <AdditionalFiles Include="Operators\*.jlops" />
</ItemGroup>

All custom operators from all .jlops files are available to all [JsonLogicRule]-annotated types in the project. Use the custom operator in a rule just like a built-in one:

{"+":[{"discount":[{"var":"price"}, 20]}, {"var":"tax"}]}

Using with the CLI tool

Pass the --operators flag pointing to a .jlops file:

corvusjson jsonlogic rules/pricing.json \
    --className PricingRule \
    --namespace MyApp.Rules \
    --operators operators/custom-ops.jlops

Argument count validation

The code generator validates that each use of a custom operator passes the exact number of arguments declared in the .jlops definition. A mismatch produces a build error (source generator) or an exception (CLI tool).

Diagnostic messages

Code Severity Description
JLSG004 Error Failed to parse a .jlops file (invalid syntax)

Workspace and memory management

JsonWorkspace is the pooled memory container used throughout Corvus.Text.Json. When evaluating JsonLogic rules:

  • With workspace: Results are allocated from the workspace. No heap allocations on the hot path. The caller is responsible for disposing the workspace.
  • Without workspace: The evaluator creates a temporary workspace internally and clones the result before disposing it.

For best performance in tight loops or request-processing pipelines, reuse a workspace and call Reset() between iterations:

using JsonWorkspace workspace = JsonWorkspace.Create();

foreach (var request in requests)
{
    workspace.Reset();
    using var dataDoc = ParsedJsonDocument<JsonElement>.Parse(request.Json);
    JsonElement result = JsonLogicEvaluator.Default.Evaluate(
        rule, dataDoc.RootElement, workspace);
    ProcessResult(result);
}

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

Clearing the expression cache:

The evaluator caches compiled delegate trees per rule. If your application dynamically generates many unique rules over time, the cache can grow unbounded. Use ClearCache() to release all cached compilations:

var evaluator = new JsonLogicEvaluator();
// ... many evaluations with unique rules ...

evaluator.ClearCache(); // releases all cached delegate trees

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.Evaluate(rule, doc.RootElement);

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

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.Evaluate(rule, item, workspace);
    ProcessResult(result);
}

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

Don't forget AdditionalFiles for the source generator

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

<!-- ❌ Missing — generator produces JLSG001 -->
<ItemGroup>
  <None Include="Rules\discount.json" />
</ItemGroup>

<!-- ✅ Correct -->
<ItemGroup>
  <AdditionalFiles Include="Rules\discount.json" />
</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.Evaluate(rule, data, workspace);
}
Console.WriteLine(result.GetRawText()); // undefined behavior

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

Comparison with other libraries

The Corvus JsonLogic implementation is designed for high-throughput scenarios where rules are evaluated millions of times. Key differences from other .NET implementations:

Feature Corvus.Text.Json.JsonLogic JsonEverything
Evaluation model Compiled delegate tree (cached) Direct interpretation
Code generation Source generator + CLI tool Not available
Numeric precision BigNumber (arbitrary precision) decimal
Memory model Pooled (JsonWorkspace) GC-allocated
Zero-allocation hot path Yes (with workspace) No
Extended operators asDouble, asLong, asBigNumber, asBigInteger Custom operator API
Runtime extensibility IOperatorCompiler (can override built-ins) Custom operator API
Code-gen extensibility .jlops custom operator templates Not available

Benchmark summary

Measured on .NET 10.0 (13th Gen Intel Core i7-13800H) across 19 scenarios. RT = Corvus runtime (interpreted), CG = Corvus code-gen (source generator), JE = JsonEverything. Ratios < 1 mean faster than JE; lower is better.

Time comparison

Scenario JE (ns) RT (ns) CG (ns) RT/JE CG/JE
Simple var 136 38 34 0.28 0.25
Comparison 373 78 35 0.21 0.09
Arithmetic 663 202 181 0.30 0.27
String cat 356 148 136 0.42 0.38
Substr 398 113 108 0.28 0.27
Min/max 34 327 386 9.75 11.5
In (array) 513 117 107 0.23 0.21
Logic short-circuit 648 165 110 0.25 0.17
Quantifier (all) 2,488 453 388 0.18 0.16
Complex rule 903 219 165 0.24 0.18
Deep nested 5,242 106 94 0.02 0.02
Missing data 1,782 474 358 0.27 0.20
Merge (constant) 1,185 170 7 0.14 0.006
Merge (mixed) 1,231 242 254 0.20 0.21
Reduce (strings) 5,699 4,015 5,239 0.71 0.92
Array filter 3,741 804 672 0.22 0.18
Object filter 4,890 1,201 969 0.25 0.20
Map (strings) 1,108 366 352 0.33 0.32
Array map/reduce 10,017 1,146 536 0.11 0.05

Memory comparison

Scenario JE (B) RT (B) CG (B)
Simple var 248 0 0
Comparison 512 0 0
Arithmetic 656 0 0
String cat 728 0 0
Substr 560 0 0
Min/max 136 0 0
In (array) 1,472 0 0
Logic short-circuit 1,304 0 0
Quantifier (all) 2,776 0 0
Complex rule 2,040 0 0
Deep nested 10,120 0 0
Missing data 2,184 0 0
Merge (constant) 4,376 136 0
Merge (mixed) 3,800 136 136
Reduce (strings) 9,368 136 136
Array filter 4,032 136 136
Object filter 6,280 136 136
Map (strings) 2,912 136 136
Array map/reduce 12,856 0 0

Summary

  • RT is faster than JE in 18/19 scenarios (0.02×–0.71× JE), with a geometric mean of 0.21× JE across those 18.
  • CG is faster than JE in 18/19 scenarios (0.006×–0.92× JE), with a geometric mean of 0.16× JE across those 18.
  • Min/max is the notable outlier where JE is faster: JE's JsonNode stores pre-parsed double values, while Corvus's JsonElement re-parses UTF-8 bytes to double on each call. Both RT and CG still allocate 0 B vs JE's 136 B.
  • Reduce (strings) is the closest scenario at 0.71× (RT) and 0.92× (CG).
  • Memory: RT allocates 0 B in 13/19 scenarios, CG in 14/19. The remaining scenarios allocate exactly 136 B (a single JsonDocumentBuilder). JE allocates 136–12,856 B in every scenario.
  • Merge (constant) showcases compile-time constant folding: CG evaluates at 12 ns (0.007× JE) by pre-computing the entire merged array as a static field.