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:
jsonata2js.ParseError— invalid expression syntax (S0xxx); thrown by the lexer/parser, but never escapescompile/compileAlldirectly — seeJsonataCompilationErrorbelow.jsonata2js.JsonataCompilationError—compile/compileAll/compileLibraryfailed to produce a usable expression. Its.causeis the underlyingParseErrorfor a syntax error;compileAll’s error additionally carries.failures({index, source, code, message}per failing expression) and.results(the full-length array, populated at every index that DID compile) so a batch failure never silently discards what succeeded.jsonata2js.JsonataEvaluationError— thrown from inside a compiled expression atevaluate()time (T0xxx/T1xxx/T2xxxtype errors,D1xxx/D2xxx/D3xxxruntime/built-in-function errors,U1001timeout/stack-overflow).jsonata2js.JsonataLoadError— internal error turning generated source into a callable function (codeU1002); not reachable from valid JSONata input, and — unlike a syntax error — escapescompile/compileAlldirectly rather than being wrapped in aJsonataCompilationError.
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
jsonata2js.compile(source): JsonataExpressionjsonata2js.compileAll(sources: string[]): JsonataExpression[]— throws aJsonataCompilationError(see Errors) identifying every failing expression if any fail, without discarding the ones that compiled.jsonata2js.compileLibrary(definition: {[name]: string}, options?: {bindings?}): CompiledLibraryCompiledLibrary#close()— after this, every export throwsT2006instead of running.JsonataExpression#evaluate(input, bindings?)JsonataExpression#assign(name, value)→thisJsonataExpression#registerFunction(name, fn, signature?)→thisJsonataExpression#useLibrary(library)→thisJsonataExpression#setTimeout(ms)→thisJsonataExpression#setMaxDepth(n)→this(non-tail-recursion depth guardrail)JsonataExpression#getSourceJsonata(): string
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
registerFunction’s optionalsignaturevalidates argument count and base type (s/n/b/a/o/f/xcodes, union types,?/-/+modifiers), matching the built-in signature grammar — it does not implementjsonata’s full XPath-style structural type system.$now()/$millis()are stable within oneevaluate()call (including inside a nested$eval), butsetTimeout()’s deadline is only checked at function-call/trampoline boundaries — a single long-running evaluation with no function calls (e.g. a huge$count([1..N])) can run past the configured deadline before it is next checked.setTimeout()/setMaxDepth()are both opt-in (no default limit); neither bounds native regex execution time, so a pathological user-supplied regex pattern can still run for a long time.
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
compileAllso 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 | jsonata2py — docs · 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
- jsonata-jvm-compiler — the Java sibling this library is ported from.
- jsonata2py — the Python sibling of the same pipeline.
- tracked-json — Jackson JsonNode wrapper that tracks each node’s location (JsonPointer) through every navigation; includes JSONPath (RFC 9535) and JSON Patch (RFC 6902).
- Valem — deterministic reactive computation runtime built on the Java sibling compiler.
- Valem Sandbox — the hosted, zero-install demo.