jq vs JSONPath: Which JSON Query Tool Should You Use?

Last updated:

jq and JSONPath both let you query JSON — but they serve different purposes. JSONPath selects nodes; jq transforms entire documents. This guide compares syntax, power, language support, and real-world use cases so you can pick the right tool for the job.

1. Syntax at a Glance

Given this document:

{
  "store": {
    "books": [
      { "title": "JavaScript: The Good Parts", "price": 12.99, "inStock": true },
      { "title": "You Don't Know JS", "price": 9.99, "inStock": false },
      { "title": "Eloquent JavaScript", "price": 14.99, "inStock": true }
    ]
  }
}

Common queries side by side:

TaskJSONPathjq
All book titles$.store.books[*].title.store.books[].title
First book$.store.books[0].store.books[0]
Books under $13$.store.books[?(@.price < 13)].store.books[] | select(.price < 13)
Only title + priceNot directly (returns full objects).store.books[] | {title, price}
Total priceNot supported[.store.books[].price] | add
In-stock titles only$.store.books[?(@.inStock)].title.store.books[] | select(.inStock) | .title

2. JSONPath Deep Dive

JSONPath uses a dollar-sign root ($) and dot or bracket notation to navigate:

$                    — root
$.foo                — property access
$.foo.bar            — nested access
$['foo bar']         — bracket notation (for keys with spaces)
$.books[0]           — array index
$.books[-1]          — last element (RFC 9535)
$.books[0,2]         — union (index 0 and 2)
$.books[1:3]         — slice (index 1, 2)
$.books[*]           — all array elements
$..title             — recursive descent: all "title" at any depth
$.books[?(@.price<10)]  — filter expression

JSONPath always returns a nodelist — a list of matched values. An expression that matches nothing returns an empty list, never an error (per RFC 9535). This makes it safe to use in pipelines where a key may or may not exist.

3. jq Deep Dive

jq filters are chained with |. Each filter receives input and produces output:

.                    — identity (pass through unchanged)
.foo                 — property access
.foo.bar             — nested
.foo?                — optional (no error if missing)
.foo // "default"    — alternative operator
.books[]             — iterate array
.books[0]            — index
.books[0:2]          — slice
.books | length      — array length
.books | map(.title) — transform each element
select(.price < 10)  — filter (keeps matching items)
{title, price}       — build new object (shorthand)
{title: .title, p: .price}  — build with renamed keys
[.books[] | .price] | add   — sum all prices
.books | group_by(.inStock) — group array
@base64              — encode as base64
@json                — convert to JSON string
@csv                 — convert to CSV row

jq also supports def for reusable functions, if/then/else/end conditionals,try/catch error handling, reduce, label-break for early exit, andenv for environment variable access.

4. Transformation: Where jq Wins

The defining jq capability is reshaping data. Here are patterns JSONPath cannot express:

# Rename keys + add computed field
.books[] | {
  name: .title,
  discountedPrice: (.price * 0.9 | . * 100 | round / 100)
}

# Group by field and count
.books | group_by(.inStock) | map({
  inStock: .[0].inStock,
  count: length
})

# Flatten nested structure
.store | to_entries | map({key, value: .value.books[0].title})

# Merge two objects
{"new": "field"} + .existingObject

# Conditional transform
.books[] | if .price > 12 then . + {tier: "premium"} else . + {tier: "budget"} end

5. JSONPath in Applications

JSONPath is the right choice when you need to integrate querying into an application. Native libraries avoid subprocess overhead and are easier to deploy:

# Python — jsonpath-ng (RFC 9535 compatible)
pip install jsonpath-ng

from jsonpath_ng import parse
expr = parse("$.store.books[?(@.price < 13)].title")
matches = [m.value for m in expr.find(data)]

# JavaScript — jsonpath-plus
npm install jsonpath-plus

import { JSONPath } from 'jsonpath-plus'
const titles = JSONPath({ path: '$.store.books[?(@.price<13)].title', json: data })

# Java — Jayway JsonPath
JsonPath.read(json, "$.store.books[?(@.price < 13)].title")

JSONPath expressions are also first-class in several specs: AWS Step Functions uses JSONPath for input/output filtering; Kubernetes uses JSONPath in kubectloutput formatting; OpenAPI 3.1 uses JSON Pointer (a JSONPath subset) for $ref.

6. jq in Shell Pipelines and CI

jq excels in shell scripts and CI pipelines where you process JSON from APIs or CLI tools:

# Extract a field from curl response
curl -s https://api.example.com/releases/latest | jq -r '.tag_name'

# Pretty-print + filter from a log file
cat app.log | jq 'select(.level == "error") | {time, message}'

# Transform and write new JSON
jq '.items[] | {id, name}' input.json > output.json

# Update a field in-place
jq '.version = "2.0.0"' package.json

# Count errors by type
jq -s '[.[].type] | group_by(.) | map({type: .[0], count: length})' errors.json

# Compact output (no whitespace) for piping
jq -c '.data[]' large.json

7. Decision Guide: jq vs JSONPath

Use this table to pick the right tool:

ScenarioPickWhy
Shell script / CI pipelinejqNative CLI, no extra deps, powerful transforms
In-process querying in Python/JS/JavaJSONPathPure library, no subprocess, easy deploy
Spec requirement (AWS, k8s, OpenAPI)JSONPathBuilt into the spec
Reshape / aggregate / computejqJSONPath only reads, does not transform
Stream very large filesjqStreaming mode avoids full load
Config-driven query (stored in DB)JSONPathSimpler, safer to eval as data
One-off data explorationjqREPL-like, rich output formatting

Frequently Asked Questions

What is the main difference between jq and JSONPath?

JSONPath is a query language — it selects and extracts nodes from an existing JSON document, analogous to XPath for XML. It returns the matched values unchanged. jq is a full transformation language — it can select, filter, reshape, compute, reduce, and generate JSON. A JSONPath expression like $.store.book[?(@.price < 10)] returns the original book objects that match; a jq filter like .store.book[] | select(.price < 10) | {title, price} returns only the title and price fields, reshaped into new objects. If you only need to extract data without changing its shape, JSONPath is simpler. If you need to transform, aggregate, or compute new values, jq is the right tool. JSONPath is widely available as a library in most languages; jq is primarily a CLI tool, though ports exist for several languages.

What is JSONPath RFC 9535 and why does it matter?

RFC 9535, published in February 2024, is the first formal IETF specification for JSONPath. Before it, JSONPath had no standard — every library (jsonpath-ng for Python, jsonpath for Go, jsonpath in JavaScript) had its own interpretation of edge cases: how filter expressions work, what the @ operator means in nested contexts, how recursive descent (..) handles duplicate nodes, and whether negative array indices are allowed. RFC 9535 resolves all of these ambiguities. It defines the syntax precisely using ABNF, specifies the normalized path representation, and clarifies that a JSONPath query always returns a nodelist (possibly empty), never an error for a missing path. Libraries that implement RFC 9535 (jsonpath-plus 9+, nimma in Node.js, jsonpath-rfc9535 in Python) produce consistent results. When choosing a JSONPath library, prefer one that explicitly advertises RFC 9535 compliance.

What can jq do that JSONPath cannot?

jq can do anything JSONPath can plus: (1) Transform shape — build entirely new objects/arrays from input values. (2) Compute — arithmetic, string interpolation, date formatting, base64 encode/decode, regex match. (3) Reduce — sum, count, group, or accumulate across an array with reduce or group_by. (4) Multiple outputs — pipe (|) chains multiple filters; filters that produce streams can generate multiple output values. (5) Conditionals — if/then/else, try/catch for error handling. (6) Define reusable functions with def. (7) Update in place — the update operator (.foo |= . + 1) modifies part of a structure without touching the rest. (8) Stream large files without loading the entire document. JSONPath's filter expressions (@.price > 10) cover simple comparisons, but they cannot aggregate, reshape, or compute derived values.

When should I use JSONPath instead of jq?

Prefer JSONPath when: (1) You are working inside an application using a language-native library — jsonpath-ng (Python), com.jayway.jsonpath (Java/Android), jsonpath (Go), jsonpath-plus (JavaScript) are easier to integrate than shelling out to jq. (2) The spec you are implementing references JSONPath — JSON Schema (draft 2020-12 uses JSON Pointer, but JSON Schema's "$dynamicRef" and some validators use JSONPath-like syntax), Kubernetes CRD validation, OpenAPI 3.1 path expressions, and AWS Step Functions JSONPath all build on JSONPath semantics. (3) You need a standardized wire format — a JSONPath expression is a string you can store in a database, pass as a config value, or include in an API request body. A jq filter is a mini-program that is harder to treat as data. (4) You only need to read/select, not transform — JSONPath is simpler to learn, audit, and sandbox.

How does jq recursive descent compare to JSONPath ..?

Both jq and JSONPath use .. for recursive descent, but with different semantics. JSONPath's .. operator traverses all descendants and collects every node that matches the following path step — $..[?(@.type=="error")] finds all "type":"error" nodes at any depth. jq's .. emits every node in the tree (including intermediate objects and arrays), which you then pipe into a filter: .. | objects | select(.type == "error"). This is more verbose but gives you access to intermediate nodes that JSONPath's .. skips. For deep-search queries, JSONPath's .. is more concise; jq's .. is more flexible because you can apply any transformation to each level. One practical difference: jq's .. on a large document can be slow because it materializes every node; JSONPath implementations typically use lazy evaluation and stop early when possible.

Can I use jq inside a Python or JavaScript program?

The primary jq distribution is a C binary, so using it inside a program requires either subprocess calls or a native port. In Python, the jq package (pip install jq) wraps libjq and lets you call jq.first(".foo", data) and jq.all(".bar[]", data) without subprocess. In Node.js, node-jq calls the jq binary via child_process — simpler to use but requires jq installed on the system. In Go, gojq is a pure-Go jq implementation you can import directly. In Rust, jaq is a jq-compatible implementation. For browser/Deno environments, jq-web compiles jq to WebAssembly. In most application code, if you find yourself reaching for jq in-process, consider whether a JSONPath library fits better — JSONPath libraries are pure-language, no binary dependency, and easier to bundle and deploy.

What is the @ operator in JSONPath filter expressions?

In a JSONPath filter expression (inside [?( )]), the @ symbol refers to the current node being tested — the element of the array currently under consideration. $.books[?(@.price < 20)] means "select from the books array every element where that element's price property is less than 20." The @ can be chained: $.books[?(@.author.name == "Tolkien")] accesses a nested property on the current element. RFC 9535 specifies that @ is only valid inside filter expressions and always refers to the current item, never to the root (which uses $). A common mistake is using $ where @ is needed inside a filter, which causes the filter to compare against the entire document root instead of the current array element.

Which is faster: jq or a JSONPath library?

For large documents (>1 MB), jq's C implementation is typically faster than pure-language JSONPath libraries, especially for complex queries. jq also supports streaming mode (--stream flag or the input/inputs builtins) that parses JSON as a stream without loading the entire document into memory — critical for files that do not fit in RAM. JSONPath libraries in interpreted languages (Python jsonpath-ng, JavaScript jsonpath-plus) load the entire document and build an in-memory tree before evaluating, which dominates runtime for large files. JSONPath libraries written in compiled languages (Go, Rust, Java with Jayway) close the gap significantly. For typical API response sizes (< 100 KB), both are fast enough that the choice should be driven by integration ergonomics, not performance. Benchmark your specific query and document size if performance is a concern.

Further reading and primary sources