JSON Query Languages: JSONPath, jq, JSONata, and SQL
Last updated:
Four languages dominate JSON querying: JSONPath (read-only path expressions, RFC 9535), jq (transformation and filtering CLI), JSONata (XPath-inspired with transformation, used in Node-RED and AWS), and SQL (via JSON functions in PostgreSQL, MySQL, and SQLite). JSONPath is supported natively in 15+ languages and AWS Step Functions; jq handles complex transformations that JSONPath cannot. JSONata processes 10,000 records in approximately 50 ms on a 2024 MacBook and supports grouping, sorting, and aggregation that jq requires multiple pipes to achieve. This guide compares all four — syntax, transformation power, language support, performance, and when to choose each — with a decision matrix included.
1. JSONPath (RFC 9535)
JSONPath selects nodes from a JSON document using path expressions rooted at $. RFC 9535, published by the IETF in February 2024, is the first formal standard for JSONPath, resolving ambiguities that caused inconsistent behavior across libraries for over 15 years.
Core syntax elements:
$ — root of the document
$.store.books — dot notation property access
$['store']['books'] — bracket notation (required for keys with spaces)
$.store.books[0] — array index (zero-based)
$.store.books[-1] — last element (RFC 9535 addition)
$.store.books[0,2] — union: index 0 and index 2
$.store.books[1:3] — slice: index 1 and 2 (stop is exclusive)
$.store.books[*] — wildcard: all array elements
$..title — recursive descent: all "title" at any depth
$.store.books[?(@.price < 13)] — filter expressionJSONPath always returns a nodelist — a list of matched values, possibly empty. A missing path never throws an error per RFC 9535, making it safe in pipelines where keys may be absent. Filter expressions use @ to refer to the current node being tested inside[?( )]: $.books[?(@.author.name == "Tolkien")] selects books whose nested author.name equals the target string. Libraries with confirmed RFC 9535 support include jsonpath-ng (Python), jsonpath-plus 9+ (JavaScript), nimma (Node.js), and Jayway JsonPath 2.9+ (Java).
JSONPath is the query language of choice for AWS Step Functions (InputPath, OutputPath, ResultPath), Kubernetes kubectl output formatting, and Grafana JSON data source queries. Its read-only nature makes it safe to store as a string in a database or pass as a configuration value — a JSONPath expression cannot mutate data. For more, see the JSONPath cheat sheet.
2. jq: Filter Pipelines and Transformation
jq is a lightweight, C-based command-line JSON processor that functions as a full programming language. Filters are chained with |; each filter receives input from the previous and passes output to the next. The jq binary is available on macOS via Homebrew (brew install jq), on Linux via apt/yum/dnf, and on Windows via winget.
. — identity: pass input unchanged
.foo — property access
.foo.bar — nested access
.foo? — optional: no error if .foo is missing
.foo // "default" — alternative: use "default" if .foo is null/false
.books[] — iterate array (emit each element)
.books | length — array or string length
.books | map(.title) — transform each element, collect into array
select(.price > 10) — keep elements where condition is true
{title, price} — build new object (shorthand for {title: .title, price: .price})
{name: .title, p: .price} — build object with renamed keys
[.books[].price] | add — sum all prices
.books | group_by(.inStock) — group array by field
.books | sort_by(.price) — sort array by field
@base64 — encode as base64
@json — convert value to JSON string
reduce .[] as $x (0; . + $x) — fold/accumulatejq also supports def for reusable functions, if/then/else/end conditionals, try/catch error handling, label-break for early exit, and --stream for parsing large files without loading them fully into memory. The update operator (.foo |= . + 1) modifies part of a structure while leaving the rest untouched. See jq filter examples for a cookbook of common one-liners.
3. JSONata: XPath-Inspired Transformation
JSONata was created by IBM and is the native expression language of Node-RED, IBM App Connect, and AWS EventBridge. It uses an XPath-inspired dot-notation that implicitly iterates arrays, making common paths concise. The jsonata npm package is a pure JavaScript library with no native dependencies.
// Installation
npm install jsonata
// Basic usage
const jsonata = require('jsonata');
const expr = jsonata('Account.Order.Product.Price');
const result = await expr.evaluate(data); // returns array of all prices
// Arithmetic — implicit array iteration
Account.Order.Product.Price * 0.9 // 10% discount on each
// Aggregation
$sum(Account.Order.Product.Price) // total of all prices
$count(Account.Order) // number of orders
$max(Account.Order.Product.Price) // highest price
// Grouping (equivalent to SQL GROUP BY)
Account.Order{ Category: $sum(Product.Price) }
// Conditional
Account.Order.Product.(Price > 50 ? "premium" : "standard")
// String operations
$uppercase(Account.Name)
$join(Account.Order.Product.Name, ", ")JSONata processes 10,000 records in approximately 50 ms in a Node.js process on modern hardware — fast enough for real-time API transformations. Its aggregation and grouping capabilities make it the most SQL-like of the JSON-native languages. AWS EventBridge uses JSONata in event patterns and transformer rules; if you are building event-driven pipelines on AWS, JSONata is already part of the toolchain.
4. SQL JSON Functions
All major SQL databases now support querying JSON stored in columns, letting you filter and extract without pulling data into application memory. Syntax varies by database:
| Task | PostgreSQL (jsonb) | MySQL | SQLite |
|---|---|---|---|
| Extract text value | data ->> 'name' | JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) | json_extract(data, '$.name') |
| Nested path | data #>> '{address,city}' | JSON_EXTRACT(data, '$.address.city') | json_extract(data, '$.address.city') |
| Containment check | data @> '{"status":"paid"}' | JSON_CONTAINS(data, '"paid"', '$.status') | No native operator |
| JSONPath query | jsonb_path_query(data, '$.items[*].price') | JSON_EXTRACT(data, '$.items[*].price') | Limited array support |
| Index support | GIN index on jsonb column | Functional index on JSON_EXTRACT() | No JSON-specific index |
PostgreSQL's jsonb type is the most powerful: it stores JSON in a binary format, supports GIN indexing for fast containment and path queries, and implements RFC 9535-style JSONPath via jsonb_path_query(). For repeated queries on the same dataset, a GIN-indexed jsonb column can outperform all three JSON-native query languages because the index avoids full document scans. See the PostgreSQL JSON guide for indexing strategies.
5. Side-by-Side Comparison
| Feature | JSONPath | jq | JSONata | SQL JSON |
|---|---|---|---|---|
| Read / select | Yes | Yes | Yes | Yes |
| Reshape / transform | No | Yes | Yes | Limited |
| Aggregation ($sum, count) | No | Yes (add, reduce) | Yes (built-in functions) | Yes (SUM, COUNT, etc.) |
| Grouping | No | group_by() | Object constructor | GROUP BY |
| Streaming large files | No | Yes (--stream) | No | Yes (cursor-based) |
| Language support | 15+ native libraries | CLI + ports for 5+ langs | JavaScript / Node.js | SQL databases only |
| Learning curve | Low | Medium | Medium | Low (if SQL known) |
| Embeddable in config | Yes (safe string) | Risky (mini-program) | Yes (expression string) | No |
| Formal specification | RFC 9535 (2024) | Informal spec | jsonata.org spec | ISO/IEC 9075 (SQL:2016+) |
6. Decision Matrix
Use this matrix to pick the right tool for your scenario. Start with the environment constraint (CLI vs. in-process vs. database), then factor in whether you need transformation or just selection.
| Scenario | Best choice | Reason |
|---|---|---|
| Shell script / CI pipeline | jq | Native CLI, powerful transforms, no extra deps |
| In-process query in Python, Java, Go, .NET | JSONPath | Pure library, no subprocess, safe to deploy |
| Node.js transformation pipeline | JSONata | Pure JS library, rich aggregation, readable syntax |
| AWS Step Functions / EventBridge | JSONPath / JSONata | Built into the service — no code required |
| Kubernetes kubectl output | JSONPath | Built into kubectl -o jsonpath |
| Query JSON stored in a database | SQL JSON (PostgreSQL jsonb) | GIN index enables fast repeated queries |
| Reshape + aggregate without SQL | jq or JSONata | Both support grouping and aggregation |
| Stream files > 1 GB | jq | --stream avoids full memory load |
| Config-driven query (stored in DB) | JSONPath or JSONata | Expression strings are data, not programs |
| One-off exploration in terminal | jq | REPL-like, colored output, rich formatting |
7. Same Query in All Four Languages
Given an orders dataset, these examples show the same three tasks — filter orders over $100, group by category, and compute the total — expressed in each language.
Sample data:
{
"orders": [
{ "id": 1, "category": "electronics", "amount": 149.99, "status": "paid" },
{ "id": 2, "category": "books", "amount": 24.95, "status": "paid" },
{ "id": 3, "category": "electronics", "amount": 89.00, "status": "pending" },
{ "id": 4, "category": "books", "amount": 119.50, "status": "paid" }
]
}Task 1: Filter orders over $100
# JSONPath — returns matching order objects
$.orders[?(@.amount > 100)]
# jq — returns matching order objects
.orders[] | select(.amount > 100)
# JSONata — returns array of matching orders
orders[amount > 100]
# SQL (PostgreSQL jsonb)
SELECT elem
FROM orders, jsonb_array_elements(data->'orders') AS elem
WHERE (elem->>'amount')::numeric > 100;Task 2: Group by category
# JSONPath — NOT SUPPORTED (read-only)
# jq
[.orders[] | select(.amount > 100)]
| group_by(.category)
| map({ category: .[0].category, orders: . })
# JSONata
orders[amount > 100]{ category: $ }
# SQL (PostgreSQL)
SELECT data->>'category' AS category,
jsonb_agg(data) AS orders
FROM (
SELECT jsonb_array_elements(data->'orders') AS data
FROM orders_table
) sub
WHERE (data->>'amount')::numeric > 100
GROUP BY data->>'category';Task 3: Compute total amount per category
# JSONPath — NOT SUPPORTED
# jq
[.orders[] | select(.amount > 100)]
| group_by(.category)
| map({
category: .[0].category,
total: ([.[].amount] | add)
})
# JSONata
orders[amount > 100]{ category: $sum(amount) }
# SQL (PostgreSQL)
SELECT data->>'category' AS category,
SUM((data->>'amount')::numeric) AS total
FROM (
SELECT jsonb_array_elements(data->'orders') AS data
FROM orders_table
) sub
WHERE (data->>'amount')::numeric > 100
GROUP BY data->>'category';The examples illustrate why JSONPath is excluded from aggregation tasks — its read-only design has no aggregation primitives. jq achieves the result through pipeline composition; JSONata achieves it with concise built-in syntax; SQL achieves it with standard relational clauses. For a deeper comparison of jq and JSONPath syntax, see the jq vs JSONPath guide.
Key Terms
- JSONPath
- A read-only query language for selecting nodes from a JSON document using path expressions rooted at
$. Formally standardized in RFC 9535 (February 2024). Supported as a native library in Python, JavaScript, Java, Go, .NET, Ruby, and more. - jq
- A lightweight, C-based command-line JSON processor that functions as a full transformation language. Filters are chained with
|; jq can select, reshape, compute, reduce, and stream JSON. Available at jqlang.github.io/jq. - JSONata
- An XPath-inspired JSON query and transformation language created by IBM. It implicitly iterates arrays, supports arithmetic and aggregation as built-in operators, and is used as the native expression language in Node-RED, IBM App Connect, and AWS EventBridge.
- Recursive descent
- An operator (written
..in both JSONPath and jq) that traverses all descendants of a node, regardless of depth. JSONPath's..is followed by a path step to filter descendants; jq's..emits every node in the tree for further piping. - Filter expression
- A predicate applied inside
[?( )]in JSONPath or viaselect()in jq to keep only array elements that satisfy a condition. In JSONPath filter expressions,@refers to the current element being tested. - Aggregation
- The computation of a summary value (sum, count, min, max, average) across a collection of values. Supported natively by jq (
add,reduce), JSONata ($sum(),$count(),$min(),$max()), and SQL (SUM,COUNT,GROUP BY). Not supported by JSONPath. - SQL JSON functions
- Operators and functions built into SQL databases for querying JSON stored in columns. PostgreSQL provides
->,->>,#>>,@>, andjsonb_path_query(); MySQL providesJSON_EXTRACT()andJSON_UNQUOTE(); SQLite providesjson_extract().
Frequently Asked Questions
What is the difference between JSONPath and jq?
JSONPath is a read-only query language that selects nodes from an existing JSON document using path expressions like $.store.books[*].title. It returns matched values unchanged. jq is a full transformation language — it can select, filter, reshape, compute, reduce, and generate new JSON structures. JSONPath cannot rename keys, compute derived values, or aggregate; jq can do all of these via filter pipelines. JSONPath is primarily used as an in-process library embedded in applications; jq is primarily a CLI tool, though ports (gojq, jaq, node-jq) exist for in-process use.
When should I use JSONata instead of jq?
Choose JSONata when you need XPath-style arithmetic and aggregation in a JavaScript or Node.js application without spawning a subprocess. JSONata expressions like Account.Order.Product.Price * 0.9 or $sum(Account.Order.Product.Price) are more readable than equivalent jq pipelines. JSONata is the native expression language of Node-RED, IBM App Connect, and AWS EventBridge — if you are working in those ecosystems, JSONata is already available. jq is a better choice for shell pipelines, CI scripts, and streaming large files.
Does JSONPath support transformations like jq does?
No. JSONPath is a read-only query language. It selects and extracts nodes from an existing document but cannot rename fields, compute arithmetic, aggregate values, or reshape the output structure. A JSONPath filter expression like [?(@.price > 10)] can filter array elements, but the result is still the original objects — you cannot project only certain fields or compute a sum. For any transformation beyond selection, you need jq, JSONata, or a general-purpose language.
How do I query JSON stored in a PostgreSQL database?
PostgreSQL provides two JSON column types — json and jsonb — and a rich set of operators. Use ->> to extract a text value: SELECT data ->> 'name' FROM users. Use #>> for nested paths: {"SELECT data #>> '{address,city}' FROM users"}. Use @> for containment checks. For complex queries, use jsonb_path_query() which accepts RFC 9535-style JSONPath expressions. The jsonb type supports GIN indexing, making path-based queries fast even on millions of rows.
What JSON query language does AWS Step Functions use?
AWS Step Functions uses JSONPath for input and output filtering in states. The InputPath, OutputPath, and ResultPathfields accept JSONPath expressions to select portions of the state input or output. AWS EventBridge uses JSONata for event transformers. Neither service supports jq natively — if you need transformation beyond JSONPath's capability in Step Functions, invoke a Lambda function.
Can I use jq in a Node.js application?
Yes, with a few options. The node-jq package calls the jq binary via child_process and requires jq installed on the system. For a pure-JavaScript alternative, jq-web compiles jq to WebAssembly and works in Node.js and browsers without a binary dependency. For most Node.js applications, a JSONPath library (jsonpath-plus) or JSONata is easier to deploy — no binary bundling required.
What is the fastest JSON query language for large datasets?
For large files that do not fit in memory, jq with --stream is the best choice — it parses JSON incrementally without loading the entire document. For in-memory datasets, jq (C binary) is typically faster than pure-language JSONPath libraries; compiled-language implementations (Go, Rust, Java) close the gap. SQL with jsonb GIN indexing (PostgreSQL) can outperform all three for repeated queries because the index avoids full document scans. For typical API response sizes under 100 KB, all four are fast enough that ergonomics should drive the choice.
Does JSONata support aggregation like SQL GROUP BY?
Yes. JSONata has built-in aggregation functions — $sum(), $count(), $min(), $max(), $average() — and supports grouping via the { key: [values] } object constructor syntax. For example, Account.Order{ Category: $sum(Product.Price) }returns an object keyed by category with the summed price as the value — equivalent to SQL's SELECT category, SUM(price) FROM orders GROUP BY category. JSONata also supports $reduce() for custom aggregation and $sort() for ordering results.
Further reading and primary sources
- RFC 9535 — JSONPath: Query Expressions for JSON — The official IETF specification for JSONPath, published February 2024
- jq Manual — Complete reference for all jq filters, types, functions, and advanced features
- JSONata Documentation — Official reference for JSONata expressions, operators, and built-in functions
- PostgreSQL JSON Functions and Operators — Full reference for PostgreSQL jsonb operators, path queries, and indexing
- jq vs JSONPath (Jsonic) — Deep-dive comparison of jq and JSONPath syntax, power, and use cases
- JSONPath Cheatsheet (Jsonic) — Quick reference for JSONPath operators, filter expressions, and slice syntax