JSONata
Evaluate JSONata expressions against JSON values from inside DuckDB SQL.
On this page
Technical Overview
When to Reach for JSONata over json_extract
JSONata is an open query and transformation language for JSON, modelled on XPath. This extension exposes it as a single scalar function that evaluates a JSONata expression against a DuckDB JSON value. The real question isn't what it can do — it's when it earns its keep over DuckDB's native JSON functions, which are the better call for simple access. JSONata wins precisely where a json_extract chain stops being concise.
JSONata vs. chains of json_extract
DuckDB ships a rich, vectorized JSON function library — json_extract, json_extract_string, the ->/->> operators, and the JSON type. For one-field-one-path access, those are the right tool and you should not reach for this extension at all. JSONata becomes the better fit once the expression starts to fan out — its single string collapses what native JSON would express as unnest plus a subquery.
-
•
Array predicate filters:
Phone[type="mobile"].numberselects matching elements and projects in one step. The native equivalent extracts the array,unnests it, filters with aWHERE, and projects in a correlated subquery. -
•
Object reshaping: An object-construction literal builds a fresh JSON object — pulling, renaming, and computing fields together. Natively that's one
json_extract_stringper output field, which gets unwieldy past two or three fields. -
•
Embedded aggregation and higher-order ops:
$sum,$count,$maxand$map,$filter,$reduceoperate on arrays inside the document, keeping per-row roll-ups inline instead of leaving the JSON value to unnest and re-aggregate. - • The break-even: Roughly: any array predicate, any reshape, or more than two output fields tilts toward JSONata. A single field at a fixed path stays simpler in native DuckDB JSON — don't add an extension for it.
How it works
The extension is a scalar UDF — no ATTACH, no secrets, no I/O. A call parses the JSONata expression into a program, evaluates it against the supplied JSON value, and returns a JSON result. The detail that matters most for performance isn't obvious from the signature.
- • Constant expressions are parsed once: When the expression argument is a SQL constant — by far the common case — the parser runs a single time and the compiled program is reused for every row. Per-row cost is then just the JSON walk plus operator application, not a parse-and-compile on each row, so a static expression over a million rows still parses exactly once.
-
•
JSON in, JSON out: Both the input value and the return type are
JSON. Cast or unnest downstream depending on what the expression yields —result::VARCHAR,result->>'$', orunnest(result::JSON[])for arrays — exactly like any other DuckDB JSON expression. -
•
Two arities, parse cache intact: The 3-arg form takes a
bindingsJSON object whose keys become$variablenames inside the expression. This is how you pass per-query values — thresholds, ids, dates — into an otherwise static expression without breaking the parse-once optimization, since the expression text stays constant.
Scope and caveats
The extension wraps the JSONata language faithfully, but it lives inside SQL — the surrounding context is DuckDB, not Node.js. A few things are worth knowing up front.
-
•
Single scalar function surface: Everything is reached through one function — there are no helper UDFs for individual JSONata operators. The expression string carries the whole computation. (It is not a JSON loader either; use DuckDB's
read_jsonfor that.) -
•
Expressions are SQL strings: JSONata expressions live inside a SQL
VARCHARliteral, so embedded double quotes need SQL escaping. For long or fiddly expressions, store them in a CTE or a SQL variable and reference by name — and iterate in the official JSONata exerciser, whose evaluator matches what the extension runs. - • No DuckDB-UDF callbacks: JSONata's JavaScript-binding feature isn't wired up — you can't register a DuckDB function as a callable inside the expression. Stick to JSONata's built-in function library.
- • Per-row cost scales with document and expression size: Deeply-nested documents and complex expressions both cost, and they scale linearly with the data they touch. For very wide rows, materialize the JSONata projection once into a column instead of re-evaluating it in every downstream query.
Deep Dive
Technical Details
What you can do with one query
The headline pattern — reshape a nested JSON column into a clean output object in a single SQL projection:
SELECT jsonata('{ "name": FirstName & " " & Surname, "mobile": Phone[type="mobile"].number, "lifetime": $sum(Orders.(Quantity * UnitPrice))}', payload) AS profileFROM contacts;One jsonata call replaces a stack of json_extract_string projections plus an unnest subquery for the order roll-up. The expression — array filter, object construction, and an aggregation function — stays in one string.
This extension exposes JSONata as a single scalar SQL function — jsonata, in two arities. There is no ATTACH interface, no secrets, no I/O. The function operates on JSON values DuckDB already has in memory.
It is the right tool when DuckDB’s native JSON functions get verbose — array predicate filters, object reshaping, multi-step transforms. It is not a JSON loader (use DuckDB’s read_json for that) and it does not register custom DuckDB UDFs as callables inside the JSONata expression.
Architecture
The extension is a Rust-implemented scalar UDF. Each call to jsonata takes a JSONata expression string and a JSON value, parses the expression into an AST, evaluates it against the document, and returns a JSON result. There is no network, no extension state between rows, and no DuckDB type beyond the standard JSON type.
When the expression argument is a SQL constant — by far the common case — the parser runs once and the compiled program is reused for every row. Per-row cost is dominated by the JSON walk plus operator application; deeply-nested documents and large arrays scale linearly with the data they touch.
The 3-arg form, jsonata(expr, json, bindings), takes a JSON object whose keys become $variable names inside the expression. This is how you pass per-query values — thresholds, ids, dates — into an otherwise static expression so the parse cache still applies.
When to reach for JSONata vs DuckDB native JSON
DuckDB ships a rich JSON function library — json_extract, json_extract_string, the ->/->> operators, json_structure, and a JSON data type. For one-field-one-path access these are the right call — vectorized, idiomatic SQL, no extra extension.
JSONata is the better fit when the path includes any of:
- Predicate filters on arrays.
Phone[type="mobile"].numberselects matching elements and projects in one step. The native equivalent isunnestplus a subquery with aWHEREclause. - Object reshaping. Construction expressions build a fresh JSON object literal — pull, rename, and compute fields together. The native equivalent is one
json_extract_stringper output field. - Embedded aggregation.
$sum,$count,$maxoperate on arrays inside the JSON document. The native equivalent leaves the JSON value, unnests, aggregates, and returns. - Higher-order array operations.
$map,$filter,$reducekeep transforms inline. The native equivalent is a lateral subquery.
Concrete contrast — selecting the mobile number from a contact’s Phone array:
-- DuckDB native: extract the array, unnest, filter, projectSELECT ( SELECT json_extract_string(p, '$.number') FROM unnest(json_extract(payload, '$.Phone')::JSON[]) AS t(p) WHERE json_extract_string(p, '$.type') = 'mobile' LIMIT 1) AS mobileFROM contacts;-- JSONata: one expressionSELECT jsonata('Phone[type="mobile"].number', payload) AS mobileFROM contacts;For a single field at a fixed path, native is simpler. The break-even is roughly “any predicate, any reshape, or more than two output fields.”
Compared to alternatives
- DuckDB native JSON (
->>,json_extract) — the right choice for simple field-by-field access. Stays simpler and avoids the extension entirely. Use JSONata when the expression starts to fan out across array predicates or object construction. jqoutside DuckDB — same general role for JSON, different language.jqis a separate process — file in, file out. JSONata-in-DuckDB keeps the transform inside the SQL plan, vectorized, joinable, andEXPLAIN-able alongside the rest of the query.- Application-side JSONata libraries (JS, Python, etc.) — the right call when the surrounding logic already lives in the app. Bringing JSONata into DuckDB matters when the JSON sits in a column and the rest of the pipeline is SQL — no row-format conversion, no result shuttling.
Iterating on expressions
JSONata expressions live inside SQL string literals, which makes them awkward to edit in place. The official JSONata exerciser takes a sample document and gives live results — the fastest loop for any non-trivial expression. The evaluator there matches what the extension runs, so once an expression is right in the exerciser it transfers unchanged into the SQL string.
Install
INSTALL jsonata FROM community;
LOAD jsonata;
Quick Start
Extract a nested field
SELECT jsonata('Account.Name',
'{"Account":{"Name":"Firefly"}}');
Filter an array with a predicate, then project
SELECT jsonata('Phone[type="mobile"].number', payload)
FROM contacts;
Reshape JSON in one expression
SELECT jsonata('{
"name": FirstName & " " & Surname,
"mobile": Phone[type="mobile"].number
}', payload) AS reshaped
FROM contacts;
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Type | Description |
|---|---|---|
|
Functions
|
||
| jsonata() |
Evaluates a JSONata expression against a JSON document and returns the result as JSON.
|
|
No extension contents match that search.
API Reference
Function Documentation
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
Extract a nested field
The simplest form — a dotted path expression replaces a chain of json_extract calls:
SELECT jsonata('Account.Name', '{"Account":{"Name":"Firefly"}}');-- "Firefly"SELECT jsonata('Order.Product.Description.Colour', payload)FROM orders;See jsonata for the function signature.
Concatenate or compute on the way out
JSONata’s & operator concatenates strings; arithmetic operators work on numbers:
SELECT jsonata('FirstName & " " & Surname', '{"FirstName":"Fred","Surname":"Smith"}');-- "Fred Smith"SELECT jsonata('Quantity * UnitPrice', '{"Quantity":3,"UnitPrice":12.50}');-- 37.5Filter an array with a predicate
The predicate-filter syntax is JSONata’s headline feature — select matching array elements, then project, in one expression:
SELECT jsonata('Phone[type="mobile"].number', payload) AS mobileFROM contacts;SELECT jsonata('items[status="open" and amount > 100].id', payload)FROM invoices;In native DuckDB JSON, the equivalent typically needs unnest plus a subquery.
Reshape into a new object
Object construction builds a fresh JSON object literal — pick fields, rename them, compute new ones — inline:
SELECT jsonata('{ "name": FirstName & " " & Surname, "mobile": Phone[type="mobile"].number, "tier": Account.Tier}', payload) AS reshapedFROM contacts;The result is a JSON object with exactly the fields you wanted — useful for building API response shapes or normalizing third-party payloads before storage.
Aggregate inside a JSON document
Use JSONata aggregation functions to roll up arrays-in-JSON without leaving the expression:
SELECT order_id, jsonata('$sum(Order.Product.(Price * Quantity))', payload)::DOUBLE AS total, jsonata('$count(Order.Product)', payload)::INTEGER AS line_itemsFROM orders;Map, filter, reduce arrays
Higher-order functions let you transform arrays element-wise:
-- Square every priceSELECT jsonata('$map(prices, function($v) { $v * $v })', '{"prices":[1,2,3,4]}');-- Keep only the high-value rowsSELECT jsonata('$filter(items, function($v) { $v.amount > 100 })', payload)FROM invoices;Parameterize an expression at query time
The 3-arg jsonata(expr, json, bindings) form supplies external $variables:
SET VARIABLE threshold = 100;
SELECT jsonata( 'items[price > $threshold].{ "id": id, "price": price }', payload, json_object('threshold', getvariable('threshold'))) AS expensive_itemsFROM line_items;The expression stays a static string (so it parses once); the per-query value rides in via bindings.
Unnest a JSONata array result into rows
When a JSONata expression returns an array, combine it with DuckDB’s unnest:
SELECT contact_id, phoneFROM ( SELECT contact_id, unnest(jsonata('Phone.number', payload)::JSON[]) AS phone FROM contacts);Iterate before pasting
For anything more than a one-line path, prototype in the official JSONata exerciser — paste your sample document, iterate on the expression with live results, then bring it into SQL once it’s right. Same evaluator semantics as the extension.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
- Linux x86_64 aarch64
- Linux (musl) Not available
- macOS Intel Apple Silicon
- Windows x86_64
- WASM eh mvp threads
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | x86_64 | 3.77 MB |
| Linux | aarch64 | 3.36 MB |
| macOS | Intel | 1.90 MB |
| macOS | Apple Silicon | 1.74 MB |
| Windows | x86_64 | 7.74 MB |
| WASM | eh | 336.3 KB |
| WASM | mvp | 393.2 KB |
| WASM | threads | 336.1 KB |
Compressed download size from the Haybarn extension repository.
DuckDB & Haybarn
Release calendar- DuckDB v1.5.5 Haybarn 1.5.5-rc1 Supported