JSONPath Syntax and Examples: Filters, Wildcards, Recursive Descent
JSONPath picks values out of a JSON document with expressions that look like property access: `$` is the root, dots and brackets walk down to children, `[0]` and `[0:2]` address array positions, and `[?(...)]` keeps the elements that match a condition. The basics behave the same in every library. Filters, functions and the shape of the result do not, and that is where most "it worked in the tester" bugs come from, so the examples below flag each place where engines disagree.
Start at the root and select child members
The dollar sign represents the whole input document. A dot followed by a member name selects an object child, so $.store.book walks from the root to store and then book. Some tools allow a relative expression without $, but including it makes the starting point explicit.
{
"store": {
"book": [
{"title": "A", "price": 8.99},
{"title": "B", "price": 12.50}
]
}
}
$.store.book
The result is a set of matches, not a single value: one object, several values or nothing, depending on the expression. Libraries wrap it differently. jsonpath-plus in JavaScript returns an array; Jayway JsonPath in Java returns the value itself for a definite path such as $.store.book[0].title and a list for anything with a wildcard, slice or filter. Do not assume a query hands back a scalar.
Property names are case-sensitive because JSON object member names are case-sensitive. $.Store does not match store, and whitespace outside a quoted property name is syntax rather than part of the key.
Use bracket notation for awkward property names
Dot notation works for simple names. Bracket notation with a quoted string is needed when a key contains a dot, space, hyphen or another character the dot grammar treats specially.
{
"user.name": "Ada",
"display name": "Ada L",
"items": {"0": "object key, not array index"}
}
$['user.name']
$['display name']
$.items['0']
$.user.name selects member name inside object user; it does not select the literal key user.name. This is a common silent no-match when data comes from flattened configuration or analytics events.
Quote escaping follows the JSONPath implementation's grammar. When an expression is itself inside JavaScript, JSON or a shell command, the host format adds another escape layer. Inspect the exact path string received by the evaluator.
Select array indexes and slices
A numeric bracket selects an array element, with zero as the first index. Thus $.store.book[0].title selects A. An out-of-range index normally produces no match rather than a JSON null value.
$.store.book[0].title -> "A"
$.store.book[1].price -> 12.50
$.store.book[99] -> no match
# Common slice form: start included, end excluded
$.store.book[0:2]
Many implementations support Python-like slices [start:end:step], but details such as negative indexes are not universal. Check your engine before relying on [-1] for the last element.
An object key containing digits is not an array index. The current node's type determines meaning: [0] addresses an array position, while ['0'] addresses a named object member.
Use wildcards for all immediate children
The wildcard * selects every member value of an object or every element of an array at one level. $.store.book[*].title returns titles from each book element.
$.store.book[*].title
# ["A", "B"]
$.store.*
# every immediate value under the store object
$[*]
# every immediate child when the root is an array or object
Wildcard result order for arrays follows array order. JSON object members are conceptually unordered, so do not use wildcard object order as a business guarantee even if one parser preserves source insertion order.
A wildcard does not recurse. If nested objects can contain the desired name at arbitrary depths, recursive descent may find them, but it also broadens the result set and work performed.
Understand recursive descent
The .. operator searches descendants rather than only direct children. $..price finds member values named price anywhere below the root. It is concise for exploration but can select unrelated fields that happen to share a name.
$..price
# all descendant members named price
$..book[*].title
# titles under every descendant member named book
$.store.book[*].price
# narrower and more predictable when the structure is known
Prefer an explicit path in application logic when the schema is stable. Recursive descent can become slower on large documents and may silently begin returning extra data when a new nested object introduces the same key.
Duplicate object member names in raw JSON are another trap. JSONPath operates on the parsed model, and many parsers retain only the last duplicate. Reject duplicate keys before querying when preserving source distinctions matters.
Filter array elements by a condition
A filter commonly uses [?(expression)], with @ referring to the candidate element. $.store.book[?(@.price < 10)].title selects titles of books whose numeric price is below ten.
$.store.book[?(@.price < 10)].title
# ["A"]
$.store.book[?(@.title == 'B')]
# the book object whose title equals B
Filter syntax is the least portable part of JSONPath. Jayway JsonPath in Java adds operators such as in, nin and =~ for regular expressions. jsonpath-plus in JavaScript evaluates the filter as a JavaScript-like expression, which is why == there coerces types. In Python, jsonpath-ng only understands filters through its extended parser, jsonpath_ng.ext. An expression copied from one of them may parse in another and return something different.
Type comparisons matter. JSON number 10 and JSON string "10" are different values, though some implementations coerce them. Normalise source data or use explicit type-aware conditions when silent coercion could select the wrong records.
Debug no match, null and result shape
No match is not the same as matching a property whose value is JSON null. Libraries represent these differently: an empty list versus a list containing null, an undefined sentinel, or an exception in a single-value API.
{"present": null, "items": []}
$.present -> one match whose value is null
$.missing -> no matches
$.items[*] -> no matches because the array is empty
Build a failing expression one segment at a time. Test $, then $.store, then $.store.book, inspecting the type at each step. Most failures are a wrong case, an array treated as an object, or a dotted key that needed bracket notation.
Pin the library version in production and keep a couple of tests for the missing-key and null cases. An expression that works in an online tester is a prototype; the library your code actually calls has the final say.
Frequently asked questions
What does $ mean in JSONPath?
`$` is the root of the JSON document. Paths such as `$.store.book` begin there and select successive child members.
What is the difference between dot and bracket notation?
Dot notation is concise for simple member names. Bracket notation with a quoted key is required for names containing dots, spaces, hyphens or other syntax characters, such as `$['user.name']`.
How do I select every item in a JSON array?
Use an array wildcard, for example `$.items[*]`. Add another child selector such as `$.items[*].name` to return one member from every selected element.
How do JSONPath filters work?
A common form is `[?()]`, with `@` representing each candidate array element—for example `$.items[?(@.price < 10)]`. Operators and functions vary significantly between engines, so test with the production implementation.
What does recursive descent do in JSONPath?
`..` searches descendants at any depth, so `$..price` finds all members named `price`. Prefer a direct path when the schema is known because recursion may become slower and select unrelated future fields.
Why does my JSONPath return no result?
Check member-name case, the type at every segment, array indexes and keys containing dots. Build the path one segment at a time, and distinguish an empty match set from a matched property whose value is JSON `null`.