JSONPath and Querying JSON: Finding Data Deep in Large Documents
Most JSON you meet in production is deep, wide, and bigger than your screen. JSONPath lets you describe what you want — "every price in every book" — as one path expression instead of a page of nested loops and null checks. This guide covers the syntax that matters, a realistic worked example, and the trade-offs against jq and application code.
Why a query language for JSON?
Retrieving data from nested JSON in application code means chained property access, null checks at every level, and loops the moment an array is involved. It works, but it scatters knowledge of the document's structure across your codebase — move a field and you're hunting through five files.
A query language inverts that: you express the location — or the pattern — of what you need, and the library does the traversal. JSONPath is the most widely deployed option, available in some form for JavaScript, Java, Python, Go, and C#, and supported natively inside databases like PostgreSQL and SQL Server.
Core syntax: $, ., .., and brackets
Every JSONPath expression starts from $, the root of the document. From there, .name selects a child key (bracket notation ['name'] means the same thing), [n] indexes into an array, and .. is recursive descent — it matches the key at any depth. The catalog below is the running example for this guide.
{
"store": {
"name": "JsonPath Books",
"banners": ["summer-sale", "new-arrivals"],
"book": [
{
"category": "reference",
"title": "Sayings of the Century",
"author": { "name": "Nigel Rees", "country": "UK" },
"price": 8.95,
"in_stock": true
},
{
"category": "fiction",
"title": "Sword of Honour",
"author": { "name": "Evelyn Waugh", "country": "UK" },
"price": 12.99,
"in_stock": false
},
{
"category": "fiction",
"title": "Moby Dick",
"author": { "name": "Herman Melville", "country": "US" },
"isbn": "978-0-14-243724-7",
"price": 8.99,
"in_stock": true
}
],
"mug": { "color": "red", "price": 19.95 }
}
}Against this document, $.store.name returns "JsonPath Books" and $.store.book[0].title returns the first title. The expression $..title collects every title anywhere in the document, regardless of depth — that recursive descent is the one thing plain dot notation can never give you.
Wildcards, slices, and unions
The * wildcard selects all children of a node: $.store.book[*] is the whole book array, $.store.book[*].author.name is every author name, and $.store.* matches both book and mug. Negative indexes count from the end, so $.store.book[-1].title is "Moby Dick".
Slices borrow Python's convention: [start:end] selects a half-open range of array elements, with an optional third value for the step. The expression $.store.book[0:2] returns the first two books, $.store.book[1:] everything after the first, and $.store.book[-2:] the last two. Unions select several children at once — indexes like $.store.book[0,2] or keys like $.store.book[0]['title','price'] — though union support is one of the places implementations still diverge.
$.store.name -> "JsonPath Books"
$.store.book[0].title -> "Sayings of the Century"
$.store.book[-1].title -> "Moby Dick"
$.store.book[*].price -> [8.95, 12.99, 8.99]
$.store.book[0:2] -> the first two book objects
$..author.name -> all three author names, at any depth
$..[?(@.price)] -> every object carrying a priceFilter expressions
Filters are where JSONPath stops being a locator and starts being a query. A filter is a bracketed expression with a question mark, and @ refers to the object being tested: $.store.book[?(@.price < 10)] returns the two books under 10 — "Sayings of the Century" and "Moby Dick".
Existence checks select objects that merely carry a key: $..book[?(@.isbn)] finds "Moby Dick", the only book with an ISBN, while books without one are skipped rather than matched as null. Comparisons support ==, !=, <, <=, >, and >=, and the left side can be a path, not just a key — $.store.book[?(@.author.country == 'UK')].title returns the two UK titles.
Filters compose with recursive descent for heterogeneous documents: $..[?(@.in_stock == true)] matches any object, anywhere, that is in stock. That's genuinely hard to express in hand-written traversal code without knowing the schema up front — which is exactly the situation JSONPath is designed for.
JSONPath vs jq vs application code
JSONPath is an expression; jq is a language. jq can filter, reshape, group, sort, and compute — piped stages like .store.book | map(select(.price < 10)) | map(.title) — which makes it the stronger choice for ad-hoc terminal work and pipeline steps. But it runs as a separate process, and embedding shell invocations in production code is a maintenance trap.
JSONPath wins when the query needs to live inside a program or a configuration: feature-flag targeting rules, policy engines, test assertions, mock-server request matchers. It's a string you can store, validate, and evaluate — not a subprocess you shell out to.
Plain code wins when the logic needs context the document can't provide: joins against other data, permission checks, or a transformation so stateful that a declarative path would obscure it. A useful rule of thumb: use JSONPath to locate, and code to decide.
Pitfalls: the spec situation and library drift
JSONPath had no normative specification for two decades — only Stefan Goessner's original example page — so implementations diverged freely. RFC 9535, published in February 2024, finally standardizes the syntax, but many popular libraries predate it: filter capabilities, union support, string quoting, and even key-name case sensitivity vary between them. Read your library's compatibility notes before shipping an expression.
Result shapes vary just as much. Some libraries return null for a failed match, some return an empty array, and some throw; JavaScript libraries usually return an array of matches even when at most one result is possible, while PostgreSQL deliberately offers both jsonb_path_query and jsonb_path_query_first. Anything that branches on a match result needs to know which convention it's dealing with.
Performance is the final trap. Recursive descent and filters scan the entire document, so a $..[?(@.price)] over a multi-megabyte file is a full traversal on every evaluation. For hot paths, extract the data once into an index, or push the query down into the database where the data already lives.
FAQ
- Is JSONPath an official standard?
- Partly. RFC 9535, published in February 2024, defines the query syntax normatively, but it arrived long after most libraries were written, so support is uneven. Many implementations still follow the original Goessner draft from the 2000s, which is less strict about filters and unions. Check your specific library against the RFC before relying on the newer features.
- What is the difference between . and .. in JSONPath?
- A single dot is a direct child lookup: $.store.name requires name to sit exactly one level under store. Two dots is recursive descent: $..name matches name at any depth below the root, including inside array elements. Use .. when you don't know — or don't care — exactly where the key lives, and . when the structure is a contract you want enforced.
- Can JSONPath modify a document, or only read it?
- Almost all implementations are read-only, and RFC 9535 covers queries only. A few libraries bolt on update or patch operations, but they're non-standard. If you need to transform JSON rather than locate data inside it, jq or ordinary code in your language is the standard answer.
- Do any databases support JSONPath natively?
- Yes. PostgreSQL exposes JSONPath through jsonb_path_query and jsonb_path_query_first, SQL Server through JSON_VALUE and JSON_QUERY with its own path dialect, and SQLite's json_extract covers the common subset. Pushing the query into the database is usually faster than shipping the document to your application, especially for large documents.
Try these tools
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.