JSONPath, with worked examples
JSONPath is a query language for JSON, in the way XPath is one for XML. An expression starts at the root, walks down through keys and array indices, and returns every value that matches — which may be none, one, or many.
The reason to learn JSONPath is that pulling one field out of a deeply nested API response should not require writing a loop. The reason it frustrates people is that it looks like a settled standard and is not: it began as a blog post in 2007, and the formal specification arrived only in 2024. Everything below notes where that history still shows.
The document these examples run against
{
"store": {
"name": "Northgate",
"orders": [
{ "ref": "A-1", "total": 42.5, "paid": true,
"lines": [{ "sku": "PEN", "qty": 3 }, { "sku": "PAD", "qty": 1 }] },
{ "ref": "A-2", "total": 118.0, "paid": false,
"lines": [{ "sku": "DESK", "qty": 1 }] },
{ "ref": "A-3", "total": 9.99, "paid": true,
"lines": [{ "sku": "CLIP", "qty": 20 }] }
]
}
}Selectors
| Expression | Returns |
|---|---|
| $ | The whole document |
| $.store.name | "Northgate" |
| $.store.orders[0].ref | "A-1" |
| $.store.orders[-1].ref | "A-3" — negative indices count from the end |
| $.store.orders[*].ref | All three references |
| $..sku | Every sku anywhere in the document |
| $.store.orders[0,2].ref | "A-1" and "A-3" — a union of indices |
| $.store.orders[:2].ref | The first two references — a slice |
| $.store.orders[1:].total | Totals from the second order onward |
| $..lines[*].qty | Every quantity, flattened across orders |
Bracket notation is the same thing in a more verbose form, and it is the one to reach for when a key contains a space, a dot or a hyphen. Dot notation cannot express those, so $["order-total"] is not a stylistic choice but the only option.
Recursive descent
Two dots search every level below the current node rather than one. It is the most useful operator in the language and the easiest to over-use: on a large document it visits every node, and it will happily match a key of the same name in a part of the tree you did not have in mind.
Filters
A filter expression selects items matching a condition. Inside it, the current item is @, and comparisons work the way you would expect. This is where JSONPath stops being a path and starts being a query.
| Expression | Returns |
|---|---|
| $.store.orders[?(@.paid == true)].ref | "A-1" and "A-3" |
| $.store.orders[?(@.total > 100)].ref | "A-2" |
| $.store.orders[?(@.total > 10 && @.paid)].ref | "A-1" |
| $.store.orders[?(@.ref == "A-2")].total | 118.0 |
| $..lines[?(@.qty >= 3)].sku | "PEN" and "CLIP" |
| $.store.orders[?(@.discount)] | Orders where the key exists at all |
The last row is worth noting: a bare @.key inside a filter is an existence test, not a truth test. It matches whenever the key is present, including when its value is false or 0, which is a common source of results that look one too many.
Where implementations disagree
RFC 9535 finally standardised the language in 2024, but the libraries that predate it are still everywhere. If an expression works in one tool and not another, it is almost always one of these.
| Area | The disagreement |
|---|---|
| Root of a match | Some return the matched values, others return their paths |
| No match | Some return an empty list, others return null, others raise |
| Parent access | A parent operator exists in some libraries and is not in the RFC |
| Script expressions | Older libraries evaluate JavaScript inside filters; the RFC does not |
| Regular expressions | A match operator is common but was spelled differently everywhere |
| Union ordering | Whether results follow document order or the order written |
Mistakes that account for most failures
- Forgetting that a filter returns a list. Even a query matching exactly one item gives you a one-element list, and indexing it is on you.
- Using dot notation for a key containing a hyphen. It parses as a subtraction in some engines and as nothing in others.
- Quoting a number in a comparison. A string comparison against a numeric field matches nothing, silently.
- Assuming recursive descent preserves structure. It flattens; the relationship between a value and its parent object is not in the result.
- Expecting an error for a wrong path. A path that matches nothing is a valid query with an empty result, not a mistake the tool will report.
Try an expression against your own fileWith a visual builder for the syntax nobody memorises.
Questions
What is JSONPath used for?
Extracting specific values from a JSON document without writing traversal code — pulling one field out of a large API response, filtering a list by a condition, or collecting every occurrence of a key across a nested structure.
What is the difference between one dot and two in JSONPath?
A single dot moves one level down to a named child. Two dots search every level below the current node, so it finds a key however deeply it is buried — at the cost of also finding it in places you did not intend.
How do I filter an array in JSONPath?
Use a filter expression, where @ refers to the item being tested — for example selecting orders whose total exceeds one hundred. Comparison and boolean operators work inside it, and the result is always a list even when one item matches.
Is JSONPath a standard?
It is now. RFC 9535 standardised it in 2024, seventeen years after the original proposal. Many widely used libraries predate it and differ on filters, error handling and whether a query returns values or paths.