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 60× faster than the jsonata interpreter on a realistic analytical benchmark, and 9.4×-87× 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.
npm install jsonata2js
Current version: 0.1.2.
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));
}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).
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);expr.setTimeout(1000); // throws U1001 if evaluation exceeds 1000msEvery 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 docs/error-codes.md 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'
}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.
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.
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.
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-19 after the code-review fixes:
| Metric | jsonata2js | jsonata |
|---|---|---|
| Compilation | ~10 ms | ~2 ms |
| 100,000 evaluations | ~1,550-1,630 ms | ~144,000-154,000 ms |
| Throughput | ~61,600-64,500 eval/s | ~650-700 eval/s |
| Speedup | ~91×-97× faster | baseline |
Figures are the range across six consecutive runs (61,620, 63,001, 63,245, 63,322, 63,743 and 64,486 eval/s; 91.0×, 91.5×, 94.4×, 96.3×, 96.8× and 97.3×). jsonata2js's own throughput is unchanged against the 55,700-65,800 eval/s measured on 2026-09-05; the ratio is higher only because the reference interpreter measured 650-700 eval/s this session against 1,040-1,090 then. Read the speedup as "tens of times", not as a figure that reproduces to two digits.
The code-review fixes are throughput-neutral: three pairs alternating the pre-fix and fixed builds in one session gave means of 62,700 and 63,400 eval/s, a ~1% difference that flips direction between pairs and sits inside the run-to-run spread. If any of it is real, the likely cost is the deadline check now made inside the higher-order and path loops — which is what stops a purely iterative expression from running past setTimeout.
Per-shape (npm run test:bench, same interpreter), median of three runs on 2026-09-19: path navigation 9.4×, predicate filter 28.4×, aggregation 84.0×, $map/$count 50.7×, sort 87.0×. $map/$count is the noisiest shape on this machine (48.3×-64.6× across the three runs); every other shape reproduced within a few percent.
jsonata's own throughput varies noticeably more run-to-run (650-700 eval/s this session, 1,042-1,091 on an idle machine on 2026-09-05) 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, and it is jsonata2js's own figure that should be compared between sessions. 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.
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).
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 |
~60× vs jsonata |
| Python | jsonata2py — docs · PyPI · source | Python source, compiled by the host compile() |
~61× 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.
MIT — see LICENSE. The vendored runtime logic comes from jsonata (MIT).