A real extraction request
The backend returns a deeply nested order response, and you want to pull out, in one go, "all order IDs with status paid plus their totals":
{
"orders": [
{ "id": "o1001", "status": "paid", "total": 128.5 },
{ "id": "o1002", "status": "pending", "total": 59.0 },
{ "id": "o1003", "status": "paid", "items": [
{ "sku": "A-01", "price": 39.9 },
{ "sku": "A-02", "price": 20.1 }
], "total": 60.0 }
]
}
In code you would write a for loop plus a condition plus a collect list—several lines. With JSONPath, one line:
$.orders[?(@.status == "paid")]. {id, total}
No single plain path can express that—it needs traversal + filtering + projection. JSONPath is designed so you can do exactly this in one string, which is perfect for API debugging, log cleaning and config extraction.
Syntax quick reference
| Expression | Meaning | Returns |
|---|---|---|
$ |
root node | the whole document |
.key / ['key'] |
child property | that property |
* |
wildcard | all children / elements |
..key |
recursive descent | matching keys at any depth |
[0] |
array index | that element |
[0,2] |
multiple indexes | elements 0 and 2 |
[start:end] |
slice | the range |
[?(@.k==v)] |
filter | elements matching the condition |
Walking through examples
$ → the whole object
$.orders[1].id → "o1002"
$.orders[*].id → ["o1001","o1002","o1003"]
$.orders[?(@.total>60)].id
→ ["o1001","o1003"]
$-orders..price → [39.9,20.1] (collect every price recursively)
Note how ..price also digs out the price nested inside items—that is exactly the value of recursive descent.
Key concepts: $ and @
$always starts from the root;$.a.b.cis an explicit path;@means "the current element" only inside a filter;- Mixing them lets you "filter a subset by condition, then pick a field inside it".
Going further: expressions and logic
$.orders[?(@.status=="paid" && @.total>=60)].id
$..[?(@.status=="paid")] # filter paid at any depth
Combine conditions with boolean logic, and you can even filter the whole document recursively—that is how JSONPath finders in tool sites let you write a screen in one line.
What if the match is empty
The query syntax does not validate whether a key exists; two common situations:
- Returns
[]: the path is valid but nothing matched (e.g. no element passed the filter); - Throws: the path itself is illegal (e.g. an index applied to something that is not an array).
To judge whether a JSONPath is correct, first format the JSON so the structure is readable, then break the path into segments and widen each level with wildcards to see where it breaks. That is exactly the "format first, then JSONPath search" combination.
A practical workflow
When debugging a third-party API, my extraction order is:
- Format the response first to see the overall levels (avoid counting braces in minified JSON);
- Use coarse wildcards like
$[*].to confirm field-name spelling; - Then land on an exact path or a filter expression;
- Cross-check: run the same path against a JSON diff to confirm two responses are consistent.
Self-check
Throw the opening orders example into any JSONPath tool:
$.orders[*].idshould return the 3 ids;$.orders[?(@.status=="paid")].idshould return 2 (o1001, o1003);$.orders..priceshould also pull out the nested 39.9 and 20.1.
If all three are right, your intuition for wildcards, recursion and filters is solid.