jslt-js
JavaScript port of JSLT — the JSON query and transformation language. Faithful translation of the Java engine: same grammar, same builtins, same semantics. Isomorphic (browser + Node.js), zero runtime dependencies.
Try it in the playground — live JSLT editor with syntax highlighting, error markers and shareable links.
For Java developers
If you know the Java JSLT library this maps directly:
| Java | JS |
|---|---|
Parser.compileString(source) | compile(source, name) |
expression.apply(input) | expr.applyInput(fromJS(input)) |
expression.apply(variables, input) | expr.applyVariables(vars, fromJS(input)) |
new ObjectMapper().readTree(json) | readTree(json) |
Jackson JsonNode tree | JsonNode tree (same API shape) |
toJson() | toJS() |
Quick start
import { compile, fromJS, toJS } from "jslt-js";
const expr = compile(`.name + " wins"`, "example.jslt");
const result = toJS(expr.applyInput(fromJS({ name: "Alice" })));
// → "Alice wins"
Install
npm install jslt-js # ESM (Node 18+, browsers)
Browser (classic script / file:// / no bundler): use the prebuilt IIFE — it sets
globalThis.JSLT (same shape as the ESM API, including extensions):
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jslt-bundle.js"></script>
<script>
const expr = JSLT.compile('{"hi": .name}');
console.log(JSLT.toJS(expr.applyInput(JSLT.fromJS({ name: "Ada" }))));
</script>
Do not rely on https://esm.sh/jslt-js@…?bundle for browsers — older publishes pulled
node:crypto into the graph. Prefer dist/jslt-bundle.js (or ESM from [email protected]+).
CJS consumers (require()):
const { compile, fromJS, toJS } = require("jslt-js");
The CJS build is at dist/jslt.cjs and is selected automatically via the exports field.
API
compile(source, sourceName, options?)
Parses and compiles a JSLT template. Returns an ExpressionImpl.
const expr = compile(source, "my-transform.jslt", {
resolver: { resolve: (name) => fs.readFileSync(name, "utf8") },
functions: myCustomFunctions, // array of Function objects
});
Options:
| Option | Type | Description |
|---|---|---|
resolver | { resolve(name): string } | Loads imported modules by name |
functions | Function[] | Custom/extension functions to register |
objectFilter | JsonFilter or string | Which object values are kept in output objects. Default drops null, {} and []. A string is a JSLT expression returning true for values to keep, as in Java's Parser.withObjectFilter(String) |
Object key filter
By default, object keys whose value is null, {} or [] are omitted from the output. To change that, pass objectFilter:
import { compile, TrueJsonFilter } from "jslt-js";
compile(source, name, { objectFilter: new TrueJsonFilter() }); // keep every key
compile(source, name, { objectFilter: "true" }); // same, as a JSLT expression
compile(source, name, { objectFilter: ". != null" }); // drop only nulls
DefaultJsonFilter, TrueJsonFilter, JsltJsonFilter (wraps a compiled expression) and the JsonFilter base class are exported.
expr.applyInput(inputNode)
Apply the compiled expression to a JsonNode input.
expr.applyVariables(vars, inputNode)
Apply with external variables. vars is a plain JS object mapping variable names to JsonNode values:
expr.applyVariables({ foo: fromJS(42) }, fromJS(null));
fromJS(value) / toJS(node)
Convert between plain JS values and the internal JsonNode tree. toJS is safe for JSON.stringify
large integers are downgraded from BigInt when they fit in a JS number.
readTree(jsonString)
Parse a JSON string to a JsonNode. Equivalent to Jackson's ObjectMapper.readTree for most purposes.
Note: uses JSON.parse internally, so whole-valued floats ("42.0") come back as IntNode(42) — the same type-erasure limitation
as any JS JSON parser. JSLT expressions and templates are not affected since the engine uses its own type-aware lexer for literals.
Imports
JSLT import statements are resolved via the resolver option:
// File-system resolver (Node only)
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
const jsltDir = dirname(jsltFilePath);
const opts = {
resolver: { resolve: (name) => readFileSync(resolve(jsltDir, name), "utf8") },
};
const expr = compile(source, jsltFilePath, opts);
Extension functions
Register custom functions the same way as Java's Parser.withFunctions:
const myFn = {
getName: () => "greet",
getMinArguments: () => 1,
getMaxArguments: () => 1,
call: (_input, [nameNode]) => fromJS("Hello " + nameNode.asText()),
};
const expr = compile(`. | greet(.name)`, "t.jslt", { functions: [myFn] });
An opt-in extension pack ships with the library. None of these are stock JSLT functions, and none reuses a built-in name (stock flatten() recurses and takes one argument, stock sum() throws on a non-number, so the pack's versions are flattened() and summed()):
| Function | Result |
|---|---|
flattened(a, b, ...) | the array arguments concatenated, one level deep; non-arrays skipped |
summed(array) | sum of the numeric elements; null if not an array or none are numbers |
enumerate(array) | [{i, v}, ...] |
range(n) | [0, 1, ..., n-1]; empty for a missing, non-number or non-positive n |
money(x) | x rounded half-up to 2 decimals; null for a non-number |
fullName(first, last) | "LAST/FIRST" upper-cased from the trimmed strings; null if both are empty |
base64Decode(s) | UTF-8 text from Base64; null for a blank or invalid input |
distinct(array) | the elements without repeats (JSON equality), first occurrence kept; [] for a non-array |
minOf(array) / maxOf(array) | smallest / largest element, nulls ignored; all numbers or all strings, else null (stock min()/max() take exactly two arguments) |
They mirror the custom functions of a Java host and are checked against the same test/resources/function-parity.json.
import { extensions } from "jslt-js/extensions";
const expr = compile(source, name, { functions: extensions });
CLI
npx jslt [--extensions] <transform.jslt> [input.json|-]
Reads JSON from input.json or stdin. --extensions loads the built-in extension pack. Imports resolve relative to <transform.jslt>'s directory.
echo '{"name":"Alice"}' | jslt greet.jslt -
cat booking.json | jslt --extensions ext.jslt -
Build
npm run build # → dist/jslt-bundle.js (browser global) + dist/jslt.cjs (Node CJS)
The browser bundle exposes globalThis.JSLT with the full API including extensions.
Tests
npm test
Runs unit tests plus the upstream conformance fixtures (query-tests.json, function-tests.json, function-declaration-tests.yaml, etc.),
vendored from schibsted/jslt — see test/resources/SOURCE.md for the exact upstream commit.
The conformance suite is the primary correctness contract for this port, not an incidental test. Every file under src/ is a line-for-line
// Port of X.java translation; "does it pass the upstream fixtures unchanged" is what defines parity with the Java engine, and
that's re-verifiable against future upstream releases by re-syncing test/resources/.
All upstream conformance fixtures pass with 0 skips. The YAML fixtures require js-yaml (devDependency).
Parity notes
- Numeric types: matches Jackson exactly —
IntNode/LongNode/BigIntegerNode/DoubleNodewith cross-typeequals(Jackson 2.6+ semantics). parse-time/format-time: implemented viaIntl.DateTimeFormat. Supports Java SimpleDateFormat tokens (yyyy MM dd HH mm ss S/SS/SSS z Z X). Returns/accepts seconds since Unix epoch (double), same as Java.- Experimental module:
import "http://jslt.schibsted.com/2018/experimental"works out of the box — providesgroup-by. FunctionWrapper: not ported — in Java it wraps a staticMethodvia reflection. In JS, any object with{getName, getMinArguments, getMaxArguments, call}is already a JSLT function, no wrapper needed.parse-url: implemented via WHATWG URL. Omitspathwhen the original URL has no explicit path (matchesjava.net.URL.getPath()returning"").- Deferred from the Java API:
ClasspathResourceResolver,FileSystemResourceResolver(use theresolveroption instead), and the REPL/playground.
Ecosystem
JSLT has a small but varied ecosystem, made by several authors, including this port's. Statuses are as of October 2026, so see each project for its current state.
Implementations
| Project | Language | Notes |
|---|---|---|
| schibsted/jslt | Java | The reference implementation and language definition. This port tracks its conformance fixtures. |
| jslt-js (this repo) | JavaScript | Pure JavaScript for browsers and Node, no native code; passes the upstream conformance suite. |
| jslt2 | Java | A VM-based implementation, mentioned in the upstream README. |
| jslt-node | Rust, Node bindings | A native N-API addon over a Rust implementation; its README calls it a proof of concept. |
Tooling and IDE plugins
- JSLT demo (source): the upstream project's playground, which runs the Java engine on a server
- jslt-editor: an embeddable CodeMirror 6 editor with highlighting, completion, hover docs, formatting and error markers.
- jslt-intellij-plugin: IntelliJ IDEs; highlighting, navigation, rename, validations and an execution dialog.
- jslt-lang: syntax highlighting for Visual Studio Code, on the Visual Studio Marketplace.
- jslt-language-server: LSP server (diagnostics, completion, hover, formatting) for any editor that uses LSP.
- jslt-playground (open it): Pure JS implementation, runs in your browser, with tabs for imports, JSON or XML input and output, and shareable links.
- jslt.tmbundle: TextMate; highlighting, symbol list, snippets, "Check Syntax" and "Apply to JSON" actions.
- Pygments: ships a JSLT lexer for
.jsltfiles, for Python and documentation tools. - vscode-jslt: Visual Studio Code and forks (Cursor, VSCodium); highlighting, live error checking, completion, hover docs, formatting, snippets and "JSLT: Apply to JSON File…".
Used with the Java library
Listed in the upstream README: the Apache Camel JSLT component, Apache NiFi as a processor, Kafka Connect transforms at Willhaben.at, IBM Cloud Pak for Business Automation and the Pincette event sourcing framework.
Learn more
A talk on the language design (slides) and a paper describing, among other things, how Schibsted uses JSLT.