JSONPath Syntax Guide: Query & Filter JSON with Examples
JSONPath is a query language for JSON, much like XPath is a query language for XML. You write a path expression and the evaluator returns every value that matches. To grab all author names from a bookstore document, you write $.store.book[*].author and you get back the list of authors, with no traversal code required.
This guide walks through every selector in JSONPath syntax with copy-paste examples you can run as you read. One thing to settle up front: there are two dialects. The 2007 Goessner dialect is the de facto classic, and RFC 9535 is the formal IETF standard published in February 2024. They agree on common paths but diverge in the edge cases, so this guide flags the differences as they come up. You can try every expression below in the JSONPath Tester and flip between both engines to compare.
Here is the selector cheat sheet to start. The rest of the article expands each row with a worked example against one shared JSON document.
| Selector | Meaning | Example |
|---|---|---|
$ | Root of the document | $ |
@ | Current element (in filters) | [?(@.price < 10)] |
.name / ['name'] | Child member | $.store.book |
.. | Recursive descent | $..author |
* | All elements / members | $.store.book[*] |
[0] | Array index | $.store.book[0] |
[start:end:step] | Array slice (half-open) | $.store.book[0:2] |
[a,b] | Union of names / indices | $.store.book[0,2] |
[?()] | Filter expression | $.store.book[?(@.price < 10)] |
length() count() match() search() value() | RFC 9535 functions (filter-only) | [?length(@.title) > 15] |
What is JSONPath?
JSONPath is a declarative query language for selecting nodes out of a JSON document. Instead of writing a loop that walks objects and arrays, you describe the location you want with a path, and the evaluator returns the matching values. The mental model is the same one XPath gives you for XML: a path made of selectors that step through the structure.
It shows up everywhere developers touch JSON. You use it to pull a field out of an API response, to assert on a value in an integration test, to address fields in pipeline configs for Kubernetes, AWS Step Functions, and Azure Logic Apps, and to extract data from large or irregular JSON without hand-writing traversal logic.
A quick note on history, because it explains the dialect split. Stefan Goessner proposed JSONPath in 2007. It spread fast and became a de facto standard, but it was never formally specified, so implementations drifted apart on the details. The IETF closed that gap in February 2024 with RFC 9535, the first formal JSONPath specification. Both dialects are alive today, which is why the same expression can behave differently depending on which library runs it.
Before you start querying, it helps to read the structure. Pretty-print messy input with the JSON Formatter so the nesting is visible.
The Example Document
Every example below runs against the classic Goessner bookstore JSON. Paste this once and reuse it:
{
"store": {
"book": [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
{ "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
],
"bicycle": { "color": "red", "price": 19.95 }
}
}
Four books with a title, author, and price, plus a bicycle. Keep this in mind: prices are 8.95, 12.99, 8.99, and 22.99, and that drives the filter results later.
Root, Child, and Recursive Descent ($ . ..)
Every expression starts at the root, written $. From there you step into children with a dot or with bracket notation. The two are equivalent:
$.store.book → the book array
$['store']['book'] → identical result
Bracket notation is what you need when a key has spaces, dots, or other special characters: $['first name'] works where $.first name would not.
The .. operator is recursive descent. It searches every level of the document, not just direct children, and collects everything that matches the following selector at any depth:
$..author
→ ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
When to use .. versus a full path
Recursive descent is convenient but blunt. $..price matches every price anywhere in the tree, including store.bicycle.price, which you may not have wanted. When you know the shape, spell out the path so the query stays precise:
$..price → [8.95, 12.99, 8.99, 22.99, 19.95] (includes the bicycle)
$.store.book[*].price → [8.95, 12.99, 8.99, 22.99] (only books)
Reserve .. for genuinely irregular or unknown structures. The trade is convenience against control: the more you know about your data, the more you should prefer an explicit path.
Wildcards, Indexes, and Array Slices (* [0] [start:end:step])
The wildcard * selects all elements of an array or all members of an object:
$.store.book[*].title
→ ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]
Array indices are zero-based, and negative indices count from the end:
$.store.book[0].title → ["Sayings of the Century"]
$.store.book[-1].title → ["The Lord of the Rings"]
Slices use the same half-open [start:end:step] convention as Python and JavaScript: start is included, end is excluded.
$.store.book[0:2].title → ["Sayings of the Century", "Sword of Honour"]
That returns two books, not three: index 0 and index 1, stopping before index 2. The exclusive end bound is the single most common JSONPath bug, so it is worth burning into memory:
[0:2] → first TWO elements (indices 0, 1) ← correct
[0:3] → first THREE elements (indices 0, 1, 2)
Omit a bound to run to the edge, and add a step for every Nth element:
$.store.book[2:].title → ["Moby Dick", "The Lord of the Rings"]
$.store.book[:3].title → first three titles
$.store.book[::2].title → ["Sayings of the Century", "Moby Dick"] (every other)
Filter Expressions [?()]
A filter [?()] keeps only the elements for which a predicate is true, and inside the filter, @ refers to the current element being tested. This is the part of JSONPath you will reach for most often.
To select books cheaper than 10:
$.store.book[?(@.price < 10)].title
→ ["Sayings of the Century", "Moby Dick"]
Against the bookstore prices (8.95, 12.99, 8.99, 22.99), two books qualify. Here is how to build filter predicates step by step:
- Compare against a literal. Use
==,!=,<,<=,>,>=, for example@.price > 10. - Match a string. String literals take single quotes:
@.author == 'Nigel Rees'. - Test for existence. A bare member reference selects elements that have it:
[?(@.isbn)]keeps only books with anisbn. - Combine conditions. Join predicates with
&&and||:[?(@.price < 10 && @.author == 'Herman Melville')].
The most frequent filter mistake is the scope of @. Inside the predicate, the current element is @, not $. Writing $.price points back at the document root, not the book under test:
$.store.book[?($.price < 10)] → wrong scope, matches nothing useful
$.store.book[?(@.price < 10)] → correct: each book's own price
RFC 9535 vs Classic in filters
The two dialects part ways on whitespace and quoting. Classic is lenient: [?(@.price<10)] with no spaces parses fine. RFC 9535 follows its grammar exactly and is stricter about how the filter is written. If a filter that worked elsewhere fails, check the spacing and the engine. Keep filters clean (spaced operators, single-quoted strings) and they evaluate the same way regardless of which library eventually runs them.
Union Selectors: Select Multiple Keys at Once ([a,b])
A union selector lists several names or indices inside one bracket and gathers all of them:
$.store.book[0]['title','author']
→ ["Sayings of the Century", "Nigel Rees"]
Unions work with indices too, and you can mix them with other selectors for a fixed projection:
$.store.book[0,2].title → ["Sayings of the Century", "Moby Dick"]
$.store.book[*]['title','price'] → title and price of every book
Unions are the right tool when you want a few specific fields rather than a whole object or a wildcard sweep.
RFC 9535 Functions: length, count, match, search, value
RFC 9535 defines five standard function extensions. There is one rule that trips up almost everyone, and that many guides get wrong:
These functions are only callable inside a filter
[?...], never as a standalone path segment.
Writing $.store.book.length() is not valid RFC 9535, and the standard grammar rejects it. That segment-call form is a jsonpath-plus extension, not part of the spec. To filter by length, you call the function inside the predicate:
$.store.book[?length(@.title) > 15]
→ [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
]
Both selected titles are longer than 15 characters; “Moby Dick” (9) and “Sword of Honour” (15, not over 15) are excluded.
Here is what each function does inside a filter:
length()— length of a string, array, or object:[?length(@.title) > 15]count()— number of nodes in a nodelist:[?(count(@.authors) > 1)]match()— whole-string regex test (I-Regexp pattern):[?match(@.author, 'J.*')]search()— substring regex test:[?search(@.title, 'the')]value()— converts a single-node nodelist to its value for comparison
All five are an RFC 9535 feature. The Classic (Goessner) dialect does not implement them, so if a function-based expression fails, confirm you are calling it inside a filter and that your engine is set to RFC 9535.
RFC 9535 vs Classic Goessner: Why the Same Expression Differs
When a JSONPath expression returns different results in two tools, the dialect is usually why. Here is how the two compare:
| Aspect | Classic Goessner (2007) | RFC 9535 (2024) |
|---|---|---|
| Standardization | De facto, never formalized | First formal IETF specification |
| Filter whitespace/quoting | Lenient ([?(@.price<10)] OK) | Strict, follows grammar exactly |
| Missing-member comparison | Implementation-defined | Well-defined, does not throw |
| Standard functions | Not part of the dialect | length count match search value |
| Normalized paths | No canonical form | Canonical, single-quoted bracket form |
| Union ordering | Varies by library | Specified |
Practical advice: if your downstream system advertises RFC 9535 compliance, write and validate against the standard engine. If you are maintaining an expression copied from jsonpath.com, jsonpath-plus, or a Jayway-based service, use Classic so results reproduce. The JSONPath Tester runs both engines behind one toggle, so you can paste an expression once and see how each dialect handles it side by side. That dual-engine comparison is the fastest way to diagnose a divergence.
JSONPath vs XPath vs jq: Which to Use
These three get conflated, so here is how they differ:
- JSONPath is a declarative path query for JSON. It is best embedded in config and test assertions where you want to name a location without writing code.
- XPath is the XML-world equivalent. JSONPath borrowed some of its notation (
*,..,[]), which is why the analogy holds, but the languages are not interchangeable and the function sets differ. - jq is a command-line JSON processor. It goes well beyond path selection into transformation and reshaping, and lives in your shell pipeline.
The decision is usually clean. For an embedded assertion or a pipeline config field, reach for JSONPath. For shell-driven transformation and data wrangling, reach for jq; the jq Cheat Sheet covers that workflow in depth. And when the question is whether a payload conforms to an expected shape rather than where a field lives, validate it with the JSON Schema Validator and its complete validation guide.
7 Common JSONPath Mistakes
- Forgot the root
$.store.bookis rejected by most engines; every expression begins at$. - Slice off-by-one.
[0:2]is two elements, not three, because the end bound is exclusive. - Wrong dialect. Running a Classic expression under RFC 9535 (or vice versa) can parse-error or match different nodes. Flip the engine to match.
- Function as a standalone segment.
$.store.book.length()is invalid RFC 9535; calllength()inside a filter. - Forgot
@in a filter.[?($.price < 10)]points at the root; use[?(@.price < 10)]. - Wrong bracket quoting.
$[store]is an error; quote the key:$['store']. - Assumed
..stops at the first level. Recursive descent matches at every depth, not just direct children.
Test JSONPath Online, Privately
The fastest way to learn JSONPath syntax is to run it. The JSONPath Tester evaluates every expression in this guide live: dual RFC 9535 and Classic engines, Values / Paths / Both result views, normalized paths for debugging, and 100% in-browser execution. It never uploads your data, never asks you to sign up, and never calls eval, so it is safe for proprietary payloads. Build a path here, confirm it selects exactly the nodes you want, then paste the validated expression straight into your code, tests, or pipeline.
For the rest of the JSON workflow, turn a sample response into typed interfaces with JSON to TypeScript, or compare two documents field by field with JSON Diff.
Frequently Asked Questions
What is JSONPath used for?
JSONPath queries JSON without imperative code. Developers use it to pull fields out of API responses, assert on values in integration tests, and address fields in configs for Kubernetes, AWS Step Functions, and Azure Logic Apps. It shines at extracting data from large or irregular structures where writing a traversal by hand would be tedious.
What is the difference between RFC 9535 and classic JSONPath?
Classic is Stefan Goessner’s 2007 de facto dialect, widely implemented but never formally specified, so libraries diverged. RFC 9535 is the IETF’s February 2024 formal specification: it defines a precise grammar, normalized paths for results, and five standard functions. The two differ at the edges in filters, unions, and missing-member comparison.
How do JSONPath filter expressions work?
A filter [?()] keeps only the elements whose predicate is true, and @ is the current element. For example, $.store.book[?(@.price < 10)] selects books priced under 10. You can combine conditions with && and ||, test whether a member exists, and compare against string or number literals.
Can I use length() as $.store.book.length()?
No. In RFC 9535, length() and the other four functions are only callable inside a filter, like $.store.book[?length(@.title) > 15]. The standalone segment form $.store.book.length() is a jsonpath-plus extension, not standard JSONPath, and the RFC 9535 grammar rejects it.
Is JSONPath the same as XPath?
No, but the idea is similar. XPath queries XML; JSONPath queries JSON; both locate nodes with path selectors. JSONPath deliberately borrowed some XPath notation (*, .., and []), which makes the analogy useful, but the syntax, semantics, and function sets are different and not interchangeable.
What does recursive descent (..) do in JSONPath?
The .. operator searches every level of the document, not just direct children. $..author collects every author member wherever it appears, at any nesting depth. It is the fastest way to pull one field out of a deeply nested or irregular structure, but it can match far more nodes than you expect, so narrow it when you can.