JSONPath Cheat Sheet: Wildcards, Recursive Descent, Slices and Filters
JSONPath is the query language for reaching into a JSON document — the thing you type when you want "every order total over 100" out of an API response without writing a loop. It is small, it is everywhere (jq-like tools, Kubernetes, AWS, Postman, test frameworks), and its edges are inconsistently implemented. This sheet covers the syntax that works the same everywhere, the parts that differ, and the questions each expression is answering.
The short version
$is the root..keyand['key']step into an object;[n]into an array;[*]or.*into every child...keysearches at every depth — the single most useful operator and the easiest to over-use.[start:end]slices like Python (end exclusive, negatives from the end);[?(…)]filters with a predicate on@, the current element.- A query returns a list of matches, even when there is one. A query that matches nothing returns an empty list, not an error.
- Filter syntax, regex support and function calls vary between implementations. RFC 9535 (2024) standardises the core; check your tool against it.
The document used below
{
"store": {
"books": [
{ "title": "Sapiens", "price": 12.5, "tags": ["history"], "stock": 4 },
{ "title": "Dune", "price": 9.99, "tags": ["sf", "classic"], "stock": 0 },
{ "title": "Refactoring", "price": 38, "tags": ["software"], "stock": 2, "isbn": "978-0134757599" }
],
"bicycle": { "color": "red", "price": 199 }
},
"updated": "2026-10-17"
}
Navigation
| Expression | Result | Notes |
|---|---|---|
$ | the whole document | Every path starts here; many tools let you omit it. |
$.updated | "2026-10-17" | Dot notation for a plain key. |
$['store']['bicycle'] | the bicycle object | Bracket notation; required for keys with spaces, dots or dashes. |
$.store.books[0] | Sapiens | Arrays are zero-indexed. |
$.store.books[-1] | Refactoring | Negative indexes count from the end. |
$.store.books[*].title | all three titles | [*] fans out over every element; the rest of the path applies to each. |
$.store.* | the books array and the bicycle | Wildcard over an object's values. |
$.store.books.length | 3 | Non-standard but widely supported convenience. |
Recursive descent
.. means "at any depth below here". $..price returns [12.5, 9.99, 38, 199] — the books' prices and the bicycle's, because it does not care where price sits. $..books[0] finds the books array wherever it is and takes the first element. $..* returns every value in the document, which is occasionally what you want and usually a sign you should narrow the path.
Recursive descent is a search, and on a large document it visits every node. In a one-off query that cost is irrelevant; in a filter applied to every message on a queue it is not. Prefer an explicit path when you know the structure, and .. when the structure varies or is unknown.
Slices and unions
| Expression | Result |
|---|---|
$.store.books[0:2] | first two books (end is exclusive) |
$.store.books[1:] | everything from the second onward |
$.store.books[:-1] | all but the last |
$.store.books[::2] | every other book (step 2) |
$.store.books[0,2] | first and third |
$.store.bicycle['color','price'] | two keys from one object |
Slices follow Python's rules exactly — [a:b] is elements a through b−1 — which is the convention the specification adopted, and the one most implementations get right. Unions of keys are supported less consistently than unions of indexes.
Filters
A filter [?(expr)] keeps the elements for which the expression is true. Inside the expression, @ is the element being tested.
| Expression | Result |
|---|---|
$.store.books[?(@.price < 20)].title | Sapiens, Dune |
$.store.books[?(@.stock == 0)] | Dune |
$.store.books[?(@.isbn)] | Refactoring — existence test, no comparison |
$.store.books[?(@.price > 10 && @.stock > 0)].title | Sapiens, Refactoring |
$.store.books[?(@.title == 'Dune' || @.title == 'Sapiens')] | two books |
$.store.books[?(!@.stock)] | Dune (stock is 0, which is falsy) |
$.store.books[?(@.title =~ /^[DS]/)] | Sapiens, Dune — regex match, where supported |
$..[?(@.price > 100)] | the bicycle — filter combined with descent |
Comparison operators are ==, !=, <, <=, >, >=; logical ones &&, ||, !; and parentheses group. String literals take single or double quotes. Comparing a string to a number is false, not an error, in most implementations — @.price > "10" silently matches nothing.
Two filter idioms are worth memorising. Existence: [?(@.key)] keeps elements that have the key with a truthy value; to keep elements that have the key even when it is 0, false or null, the RFC form is [?@.key] and some tools offer [?(@.key != null)]. Membership: there is no portable "value in list"; write the alternatives out with ||, or use the in operator where your tool provides it.
What comes back
A JSONPath query always produces a list of matches — the nodelist in RFC terms — because wildcards, descent and filters can match any number of nodes. $.updated yields ["2026-10-17"], a one-element list, not the bare string. Tools differ in how they present this: some unwrap single results, some always show the list. Both are fine as long as your code does not assume a single value from a path that could match several.
A path that matches nothing yields an empty list. It is not an error, which is convenient in scripts and a trap in tests: an assertion that $.data[0].id equals 42 may fail with "expected 42, got undefined" when the real problem is that data was renamed. Asserting on the match count first catches that.
Implementations that report paths alongside values — $['store']['books'][1]['title'] for Dune — are the ones to prefer for exploration, because the normalised path is what you paste into code.
Where implementations disagree
- Regex in filters (
=~) is an extension; the RFC uses amatch()/search()function instead. - Function calls such as
length(),min(),avg()exist in Jayway (Java) and the RFC's function extensions, not in most JavaScript libraries. - Script expressions
[(@.length-1)]from the original 2007 proposal are deprecated and widely unsupported; use[-1]. - Bracket keys: single quotes are universal; double quotes are RFC-standard but older tools reject them.
- Deep wildcards on scalars:
$.updated.*on a string returns nothing in some tools and errors in others.
If a query must run in two places — a test suite and a Kubernetes -o jsonpath, say — stay inside the navigation, descent, slice and comparison subset, which behaves the same everywhere.
JSONPath, jq and JSON Pointer
Three things get confused. JSON Pointer (RFC 6901, /store/books/0/title) addresses exactly one location and cannot wildcard or filter; it is what JSON Patch and JSON Schema's $ref use. JSONPath selects any number of nodes with wildcards and predicates, and stops there. jq is a full programming language with its own selection syntax (.store.books[] | select(.price < 20) | .title) plus transformation, arithmetic, string formatting and control flow. For "get me these values", JSONPath is the smallest tool that does the job; the moment you need to reshape the output, you want jq or code.
Related guides and tools
Frequently asked questions
$.key reads the key directly under the root and nothing else. $..key searches every object at every depth and returns each key it finds, so on a document with nested records it can return many values from different places. Use the explicit path when you know where the value lives; use descent when the structure varies or you want every occurrence.
Append a filter in square brackets with a predicate on @, the current element: $.items[?(@.status == 'paid')] keeps the elements whose status is paid, and you can continue the path after it, as in $.items[?(@.status == 'paid')].total. Combine conditions with && and ||, negate with !, and group with parentheses.
Because JSONPath always returns the set of matching nodes; a path that can only match once still returns a one-element list. Some tools unwrap single results for display. In code, take the first element explicitly, and check the count when the path could legitimately match zero or many nodes.
In many implementations, with the =~ operator and a /pattern/ literal, as in [?(@.email =~ /@example\.com$/)]. It is an extension rather than part of the original proposal; RFC 9535 provides match() and search() functions instead. If portability matters, filter on exact values and do pattern matching in the surrounding code.
No. JSONPath selects nodes and stops; jq is a programming language that selects, transforms, computes and formats, with its own syntax. For extracting values, JSONPath is simpler and is built into many tools such as kubectl, Postman and test frameworks. For reshaping JSON into a new structure, jq or ordinary code is the right instrument.