JSONPath is a query language for JSON, the same way XPath is a query language for XML. You write a short expression that describes a route through a document, and the evaluator returns every value that route reaches. It is the fastest way to pull a handful of fields out of a large payload without writing procedural traversal code.
This cheat sheet is a working reference. Every selector below runs against one sample document, and the expression table shows the exact result each query returns. We also cover RFC 9535, the 2024 specification that finally standardized JSONPath, and where its rules diverge from the older Goessner syntax you will still see in the wild.
To run these as you read, paste the document and any expression into the JSONPath / JQ Tester and watch the matches update live.
What JSONPath Is (and the RFC 9535 Standard)
Stefan Goessner introduced JSONPath in 2007 as a JSON analog to XPath. For over a decade it had no formal specification, so every library implemented it slightly differently: some supported script expressions, some did not; slices behaved inconsistently; filter syntax varied. Code that worked in one language often broke in another.
In February 2024 the IETF published RFC 9535, which defines JSONPath precisely: the grammar, the semantics of each selector, and how results are ordered. A conforming query returns a nodelist, an ordered list of the nodes (values plus their locations) that the path selects. Modern libraries increasingly target this standard, which is why the behavior described here is broadly portable.
The Sample Document
Every expression in this guide runs against the document below: a small store with an array of books and a single bicycle object. That is enough to demonstrate arrays, nested objects, mixed types, and recursive descent.
{
"store": {
"books": [
{ "title": "Clean Code", "author": "Robert Martin", "price": 32.5, "inStock": true },
{ "title": "The Pragmatic Programmer", "author": "Andrew Hunt", "price": 42.0, "inStock": false },
{ "title": "Refactoring", "author": "Martin Fowler", "price": 9.99, "inStock": true }
],
"bicycle": { "color": "red", "price": 199.95 }
}
}Root, Children, and Wildcards
Three selectors cover the majority of everyday queries: the root identifier, child access, and the wildcard.
$ — the root
Every JSONPath expression begins with $, which refers to the entire document. On its own, $ returns the whole thing. Everything after it narrows the selection.
. and ['key'] — child access
Dot notation (.store) and bracket notation (['store']) are equivalent ways to descend into an object member. Use bracket notation when a key contains spaces, dots, or other characters that would break dot notation: $['first name'] works where $.first name does not.
* — the wildcard
$.store.*selects every direct value of the store object: the books array and the bicycle object.$.store.books[*]selects every element of the books array.$.store.books[*].titleselects the title of every book.
Recursive Descent and Array Selectors
.. — recursive descent
The .. operator searches the current node and all of its descendants at any depth. $..price finds every price anywhere in the document, returning the three book prices and the bicycle price no matter how deeply nested. It is the single most useful selector for pulling one field out of an irregular payload.
Indexes and slices
Array elements are selected by numeric index inside brackets, and Python-style slices select ranges.
[0]— the first element (indexing is zero-based).[-1]— the last element. Negative indexes count from the end and are part of RFC 9535.[0:2]— a slice from index 0 up to but not including index 2, so the first two elements.[::2]— a slice with a step of 2, selecting every other element.[0,2]— a union of specific indexes, selecting the first and third elements.
Filter Expressions
Filters are where JSONPath earns its keep. A filter tests each element of a collection and keeps only those for which the test is true. Inside a filter, @ refers to the current element being tested, while $ still refers to the document root.
The classic Goessner form wraps the test in parentheses after a question mark: [?(@.price < 10)]. RFC 9535 makes the parentheses optional grouping, so [?@.price < 10] is equivalent. Many libraries only accept the parenthesized form, so it is the safest to paste into an arbitrary tool today.
$.store.books[?(@.price < 10)] -> the "Refactoring" book object
$.store.books[?(@.inStock == true)].title -> ["Clean Code", "Refactoring"]
$.store.books[?(@.price > 10 && @.inStock == false)].title -> ["The Pragmatic Programmer"]
$..[?(@.price > 100)] -> the bicycle objectFilters support comparison operators (==, !=, <, <=, >, >=), boolean logic (&& and ||), and, in RFC 9535, match() and search() functions for regular expressions. You can also test for the mere existence of a key: [?(@.discount)] keeps elements that have a discount member at all.
import { JSONPath } from "jsonpath-plus";
const data = {
store: {
books: [
{ title: "Clean Code", price: 32.5, inStock: true },
{ title: "The Pragmatic Programmer", price: 42.0, inStock: false },
{ title: "Refactoring", price: 9.99, inStock: true },
],
},
};
// Titles of every book under $10
const cheap = JSONPath({
path: "$.store.books[?(@.price < 10)].title",
json: data,
});
console.log(cheap); // => ["Refactoring"]Expression Reference Table
A quick-scan reference. Every result is what the expression returns against the sample document above.
| Expression | Result |
|---|---|
| $ | The entire document |
| $.store.books | The array of three books |
| $.store.books[0] | The "Clean Code" book object |
| $.store.books[-1] | The "Refactoring" book object |
| $.store.books[0:2] | The first two book objects |
| $.store.books[*].title | All three titles |
| $.store.* | The books array and the bicycle object |
| $..price | [32.5, 42.0, 9.99, 199.95] |
| $..author | All three author names |
| $.store.books[0,2].title | ["Clean Code", "Refactoring"] |
| $.store.books[?(@.price < 10)] | The "Refactoring" book object |
| $.store.books[?(@.inStock)].title | ["Clean Code", "Refactoring"] |
| $..* | Every value in the document, at every depth |
JSONPath vs jq
JSONPath and jq are often mentioned together, but they solve different problems. JSONPath is a selection language: it describes a set of locations in a document and returns the values there. jq is a complete, Turing-complete transformation language with pipes, variables, functions, conditionals, and the ability to construct entirely new JSON. If you only need to extract values, JSONPath is more compact. If you need to reshape, aggregate, or compute, jq is the right tool.
The syntax differs too. jq has no root symbol; a path starts with a leading dot. Iteration uses .[] rather than [*], and filtering uses the select() function fed through a pipe.
# Every price anywhere in the document
jq '.. | .price? // empty' store.json
# Books under $10
jq '.store.books[] | select(.price < 10)' store.json
# Just the titles of in-stock books
jq '.store.books[] | select(.inStock) | .title' store.json| Task | JSONPath | jq |
|---|---|---|
| Root | $ | . (leading dot) |
| Child | $.store.books | .store.books |
| All array items | $.store.books[*] | .store.books[] |
| Filter | [?(@.price < 10)] | select(.price < 10) |
| Current item | @ | . |
| Transform / build new JSON | Not supported | Fully supported |
A practical rule: use JSONPath (or the JSONPath side of the tester) when you are reading fields out of a fixed schema, and jq when the output shape differs from the input shape.
Common Gotchas and Portability
- Filter parentheses differ: Goessner uses [?(@.x)], RFC 9535 makes them optional as [?@.x]. Prefer the parenthesized form for maximum tool compatibility.
- Single value vs nodelist: some libraries always return an array of matches even when a path can match at most one node. Do not assume a scalar comes back.
- Result ordering can vary. Array elements keep index order, but because JSON object members are unordered, the order of matches spanning objects (especially recursive descent) is not guaranteed across libraries.
- Bracket-quote keys that contain dots or spaces: $['user.name'] selects the literal key "user.name", whereas $.user.name descends two levels.
- An empty result usually means the path did not match, not an error. Confirm the document is valid with the JSON Validator before assuming the query is wrong.
When in doubt, test the exact expression against the exact document rather than reasoning about it. Small differences in slice bounds, filter operators, or key escaping change the result, and the fastest feedback loop is to paste both into a live evaluator.