Skip to the content.

License: MIT

JSONata for JavaScript — translated to JavaScript source, not interpreted.

Parses a JSONata expression once, generates a plain JavaScript function for it, and loads that function in-memory with new Function — evaluating a hot, repeatedly-used expression skips per-call parse/interpret overhead the way a compiler skips it, instead of tree-walking the AST on every call the way jsonata does. Repeated evaluation of a compiled expression is around 53-60× faster than the jsonata interpreter on a realistic analytical benchmark, and 9.7×-98× faster across the more targeted per-construct benchmarks (see Performance).

Ported from jsonata-jvm-compiler (same compile pipeline, same AST shape, same error-code contract) with runtime built-ins vendored from jsonata’s own pure-JS interpreter wherever the logic is interpreter-agnostic.

Zero required runtime dependencies. Node.js >= 18.

Install

npm install jsonata2js

Current version: 0.1.1.

Quickstart

const jsonata2js = require('jsonata2js');

const expr = jsonata2js.compile('Account.Order[Price > 100].OrderID');

const result = expr.evaluate({
  Account: {
    Order: [
      { OrderID: 'o1', Price: 50 },
      { OrderID: 'o2', Price: 150 },
    ],
  },
});
// => "o2"

Compile once, evaluate many times against different input — no re-parse:

const total = jsonata2js.compile('$sum(items.price)');
for (const order of orders) {
  console.log(total.evaluate(order));
}

Bindings

const expr = jsonata2js.compile('$x & $greet(name)');

expr.assign('x', 41);                                  // permanent variable binding
expr.registerFunction('greet', (n) => `Hello, ${n}!`);  // permanent function binding

expr.evaluate({ name: 'Ada' });
// $x resolves to 41, $greet(...) calls the registered function

// One-shot bindings, scoped to a single evaluate() call:
expr.evaluate({ name: 'Ada' }, { x: 1 });

registerFunction(name, fn, signature?) accepts an optional JSONata-style <params:return> signature string. If given, argument count AND type are validated against it before every call (the same T0410 argument-signature-mismatch enforcement built-ins get) — fn.length is used for arity only when no signature is given (needed for a variadic/rest-parameter function, where fn.length isn’t reliable).

Compiling a library

compileLibrary compiles a map of { exportName: 'jsonata expression source' } (each expression must evaluate to a function value, typically a lambda literal) into a set of bound functions ready for useLibrary:

const lib = jsonata2js.compileLibrary({
  double: 'function($x){ $x * 2 }',
  greet: 'function($n){ "Hello, " & $n }',
});

const expr = jsonata2js.compile('$double($greet(name).$length())');
expr.useLibrary(lib);

Timeouts

expr.setTimeout(1000); // throws U1001 if evaluation exceeds 1000ms

Errors

Every thrown error is one of:

All four extend jsonata2js.JsonataError and expose .code (see the error-code reference for the full catalogue) plus any error-specific fields (.token, .value, .position, …) as own enumerable properties.

try {
  jsonata2js.compile('1 + + 2');
} catch (e) {
  e.code;    // "S0211"
  e.message; // 'The symbol "+" cannot be used as a unary operator'
}

API

Full type declarations: jsonata2js.d.ts.

Conformance

The official JSONata conformance suite (vendored unmodified from jsonata/test/test-suite into test/test-suite/, 102 topic groups / 1686 cases) is run via:

npm run test:suite

jsonata2js passes 100% of the vendored suite (102/102 groups, 1686/1686 individual assertions), and the runner exits non-zero if any case fails or if fewer cases than expected are discovered, so a regression here fails CI rather than just printing a lower percentage. npm test runs the unit-test suite (lexer/parser, the runtime built-in modules exercised directly, conformance regressions, and worker-thread/re-entrancy/heap-retention checks); npm run test:bench benchmarks compiled-expression throughput per construct against the jsonata interpreter; npm run test:perf runs the head-to-head analytical benchmark described below.

Known limitations

100% conformance against the vendored suite does not by itself prove there are zero remaining edge cases outside it — $match’s empty/singleton-result collapse ($match(str, /no-match/) must return undefined, not []; a call that naturally yields exactly one match must return that bare {match,index,groups} object, not a one-element array, matching every other jsonata sequence-returning built-in) was found and fixed this way, not by a suite failure, since the suite’s own $match cases only exercise multi-match results or immediately chain further field access that happens to auto-unwrap either shape.

Performance

jsonata2js compiles expressions to a plain JavaScript function loaded with new Function, so repeated evaluation skips per-call AST interpretation entirely — significantly faster than jsonata’s tree-walking interpreter for a hot, reused expression.

Benchmark: jsonata2js vs jsonata

The benchmark compiles one expression once, then runs 100,000 evaluations against the same parsed JSON document (with a 1,000-evaluation warmup before timing) — same methodology, same expression, and the same input document as the JVM implementation’s PerformanceComparisonTest, ported byte-for-byte (test/performance/benchmark_expression.jsonata, test/performance/benchmark_input.json). The expression is a realistic analytical query covering variable bindings, nested field navigation, array filtering, aggregation functions ($sum, $count, $average, $max, $min, $distinct), string operations, arithmetic, and a conditional.

Measured on Node.js v24.14.1, Windows 11, from test/performance-comparison.js (npm run test:perf) — side-by-side runs in one process, each warming up and timing both libraries back to back. Re-measured 2026-09-05 for v0.1.1, after the conformance and scan-fusion work of that day:

Metric jsonata2js jsonata
Compilation ~10 ms ~2 ms
100,000 evaluations ~1,520-1,800 ms ~92,000-96,000 ms
Throughput ~55,700-65,800 eval/s ~1,040-1,090 eval/s
Speedup ~53×-60× faster baseline

Figures are the range across three consecutive runs (55,699, 55,787 and 65,836 eval/s; 52.5×, 53.5× and 60.3×). The fastest run is the one that starts on an otherwise idle machine, which is where the top of the range comes from; the same spread was seen across the nine runs taken on 2026-09-03/04 (53,889-64,743 eval/s).

Per-shape (npm run test:bench, same interpreter), median of three runs on 2026-09-05: path navigation 9.7×, predicate filter 29.4×, aggregation 50.0×, $map/$count 57.9×, sort 79.8×. Sort is the noisiest shape on this machine (72.7×-97.8× across the three runs); every other shape reproduced within a few percent.

jsonata’s own throughput varies noticeably more run-to-run (1,042-1,091 eval/s here, and as low as ~720 on a loaded machine) than jsonata2js’s, consistent with an async tree-walking interpreter re-allocating its evaluation environment/sequence objects on every one of the ~200 sub-expressions in this benchmark for every one of the 100,000 calls, versus a compiled function with no per-call interpretation overhead. The speedup ratio therefore moves more than jsonata2js’s own absolute throughput does. Both libraries were verified to produce byte-identical JSON output before each benchmark run.

Compilation is a one-time cost paid at startup. For any workload that reuses an expression more than a handful of times, the throughput advantage dominates. Compiling several expressions at once? Use compileAll so a syntax error in one doesn’t stop the others from compiling.

Reproduce it yourself:

npm run test:perf

The reference interpreter comes from the jsonata devDependency, so npm install is all it needs; set J2JS_REF_JSONATA=/path/to/jsonata/src/jsonata.js to measure against a local checkout instead. It is not part of npm test — a 100,000-call benchmark against an async interpreter takes well over a minute, which is exactly the cost being measured.

Architecture

source string
  -> src/parser/lexer.js + src/parser/parser.js   (S0xxx errors)
  -> src/optimizer/optimizer.js                    (constant folding, structural simplification)
  -> src/translator/translator.js                  (AST -> JS source string,
     + src/translator/scan-fusion.js                 one fused pass per bound sequence)
  -> src/loader/loader.js (new Function)            (JS source -> callable function)
  -> src/index.js (JsonataExpression#evaluate)      (bindings, timeout, error mapping)

Runtime support the generated code calls into lives under src/runtime/: values.js (missing/null/sequence/truthy/equality/arithmetic semantics), path.js (tuple-based path navigation with parent-chain and @$/#$ binding tracking), hof.js / structural.js / lambda.js (higher-order functions, group-by/transform, ~> chain + tail-call trampoline), builtins.js (the assembled function registry), and the individual built-in-function modules (string.js, numeric.js, datetime.js, regex.js, codec.js, core.js, collections.js, objects.js).

Sibling implementations

The same parse → optimise → translate → compile pipeline exists for three host runtimes:

Runtime Project Host code it generates Speedup vs. that runtime’s reference interpreter
JVM jsonata-jvm-compiler (Java 21) — docs · Maven Central · source Java source, compiled in-memory by javac ~56× vs JSONata4Java
JavaScript jsonata2js (this project) — docs · npm · source a JS function, loaded with new Function ~53×-60× vs jsonata
Python jsonata2pydocs · PyPI · source Python source, compiled by the host compile() ~54× vs jsonata-python

Each figure is that project’s own measurement against its own runtime’s reference interpreter, on the same expression and input document; they are not comparable to each other as absolute speeds.

The JVM implementation is the original, and is the compiler behind valem.run’s reactive computation engine.

License

MIT — see LICENSE. The vendored runtime logic comes from jsonata (MIT).

See also