This JSONPath cheat sheet covers the syntax you need to query JSON. JSONPath is to JSON what XPath is to XML. You write a short expression that describes a route through a document, and the evaluator returns every value on that route. It is the fastest way to pull a few fields out of a large payload without writing traversal code.
Every selector below runs against one sample document, and the expression table shows the exact result of each query. The guide also covers RFC 9535, the 2024 specification that standardized JSONPath, and where it differs from the older Goessner syntax that many tools still use.
To run these as you read, paste the document and any expression into the JSONPath tester. Matches update as you type. For jq filters, use the jq tester.
What JSONPath is (and the RFC 9535 standard)
Stefan Goessner introduced JSONPath in 2007 as a JSON version of XPath. For over a decade it had no formal specification, so each library implemented it a little differently. Some supported script expressions and some did not, slices were inconsistent and filter syntax varied. Code that worked in one language often broke in another.
In February 2024 the IETF published RFC 9535. It defines the JSONPath grammar, what each selector means and how results are ordered. A conforming query returns a nodelist, an ordered list of the nodes (values plus their locations) the path selects. More and more libraries target this standard, so the behavior described here works across most of them.
The sample document
Every expression in this guide runs against the document below, a small store with an array of books and one bicycle object. That is enough to show 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 most everyday queries: the root, child access and the wildcard.
$ (the root)
Every JSONPath expression starts with $, which means the whole document. On its own, $ returns everything. Each selector after it narrows the result.
. and ['key'] (child access)
Dot notation (.store) and bracket notation (['store']) both step into an object member. Use bracket notation when a key contains spaces, dots or other characters that 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 its descendants at any depth. $..price finds every price in the document, so it returns the three book prices and the bicycle price however deeply they are nested. It is the most useful selector for pulling one field out of an irregular payload.
Indexes and slices
Select array elements by numeric index inside brackets. Python-style slices select ranges.
[0]is the first element. Indexing is zero-based.[-1]is the last element. Negative indexes count from the end and are part of RFC 9535.[0:2]is a slice from index 0 up to but not including index 2, so the first two elements.[::2]is a slice with a step of 2, so every other element.[0,2]is a union of specific indexes, so the first and third elements.
JSONPath filter expressions
Filters are the most useful part of JSONPath. A filter tests each element of a collection and keeps the ones where the test is true. Inside a filter, @ is the element being tested, and $ is still the document root.
The classic Goessner form puts the test in parentheses after a question mark: [?(@.price < 10)]. In RFC 9535 the parentheses are optional, so [?@.price < 10] means the same thing. Many libraries only accept the form with parentheses, so it is the safest choice for an unknown tool.
$.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 (==, !=, <, <=, >, >=) and boolean logic (&& and ||). RFC 9535 adds match() and search() functions for regular expressions. You can also test whether a key exists. [?(@.discount)] keeps elements that have a discount member.
More filter examples
Filters combine comparisons, string tests and boolean logic. Each line below runs against store.json and shows the titles it returns:
$.store.books[?(@.author == 'Martin Fowler')].title -> ["Refactoring"]
$.store.books[?(@.author != 'Robert Martin')].title -> ["The Pragmatic Programmer", "Refactoring"]
$.store.books[?(@.price >= 10 && @.price <= 40)].title -> ["Clean Code"]
$.store.books[?(@.inStock == false || @.price < 10)].title -> ["The Pragmatic Programmer", "Refactoring"]
$..books[?(@.price < 40)].author -> ["Robert Martin", "Martin Fowler"]
$.store.books[?(@.price > 10)].price -> [32.5, 42]String matching depends on the library. RFC 9535 defines match() for a full regular expression match and search() for a match anywhere in the string, as in $.store.books[?search(@.title, 'Code')]. Libraries that evaluate filters as JavaScript, such as jsonpath-plus (used by the JsonKit tester), take JavaScript methods instead: $.store.books[?(@.title.includes('Code'))] or $.store.books[?(@.title.match(/^R/))].
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"]JSONPath expression reference table
Each result below is what the expression returns for 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 == true)].title | ["Clean Code", "Refactoring"] |
| $..* | Every value in the document, at every depth |
JSONPath vs jq
JSONPath and jq often come up 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 Turing-complete transformation language with pipes, variables, functions and conditionals, and it can build new JSON. To extract values, JSONPath is more compact. To reshape, aggregate or compute, use jq.
The syntax differs too. jq has no root symbol, and a path starts with a dot. Iteration uses .[] instead of [*], and filtering pipes values into the select() function.
| Task | JSONPath | jq |
|---|---|---|
| Every book title | $.store.books[*].title | .store.books[].title |
| First book | $.store.books[0] | .store.books[0] |
| Books under $10 | $.store.books[?(@.price < 10)] | .store.books[] | select(.price < 10) |
| Titles by author match | $.store.books[?(@.author.includes('Martin'))].title (JavaScript-based libraries) | .store.books[] | select(.author | test("Martin")) | .title |
| Every price at any depth | $..price | .. | .price? // empty |
| Count the books | Not supported | .store.books | length |
| Sum of prices | Not supported | [.store.books[].price] | add |
| Sort by price | Not supported | .store.books | sort_by(.price) |
| Rename fields | Not supported | .store.books[] | {name: .title, cost: .price} |
| Result shape | One array of matches | A stream of values, one per line |
# 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.jsonIn practice, use JSONPath to read fields from a fixed schema. Use jq when the output shape differs from the input shape, or when you need counts, sums or sorting. You can run real jq 1.8 filters in the jq tester.
JSONPath vs jq vs JMESPath: syntax side by side
JMESPath is the third JSON query language you will meet, mostly through the AWS CLI's --query option and the Azure CLI. Like JSONPath it mainly reads data, but it has its own grammar. There is no $ root, it uses projections instead of wildcards that return nodelists, and literal values go in backticks. The table shows the same operations in all three against the sample document.
| Operation | JSONPath (RFC 9535) | jq | JMESPath |
|---|---|---|---|
| Root | $ | . | Implicit. Expressions start at the root |
| Current node | @ (inside filters) | . | @ |
| Child | $.store.bicycle.color | .store.bicycle.color | store.bicycle.color |
| Key with a space | $['first name'] | .["first name"] | "first name" |
| Array index | $.store.books[0] | .store.books[0] | store.books[0] |
| Last element | $.store.books[-1] | .store.books[-1] | store.books[-1] |
| Wildcard over array | $.store.books[*].title | .store.books[].title | store.books[*].title |
| Wildcard over object values | $.store.* | .store[] | store.* |
| Recursive descent | $..price | .. | .price? // empty | Not supported |
| Filter | $.store.books[?@.inStock == true].title | .store.books[] | select(.inStock) | .title | store.books[?inStock].title |
| Slice | $.store.books[0:2] | .store.books[0:2] | store.books[0:2] |
| Slice with step | $.store.books[::2] | No step syntax | store.books[::2] |
| Length | Only inside filters: $.store.books[?length(@.title) > 12] | .store.books | length | length(store.books) |
| Build new JSON | Not supported | {title, price} and more | {t: title, p: price} multiselect hash |
Numeric comparisons show the most visible difference. JMESPath needs literal values in backticks. jq writes them bare inside select(), and JSONPath writes them bare inside [?...]:
# JSONPath (RFC 9535)
$.store.books[?@.price < 10].title
# jq (wrap in [ ] to collect the stream into an array)
jq '[.store.books[] | select(.price < 10) | .title]' store.json
# JMESPath (number literals go in backticks)
store.books[?price < `10`].title
# All three return: ["Refactoring"]Results come back differently
- JSONPath returns a nodelist, an array of every match, even when there is only one.
$.store.bicycle.colorgives["red"]. - jq emits a stream of separate values.
.store.books[].titleprints three strings on three lines. Wrap the expression in[ ]to get one array. - JMESPath returns a single JSON value.
store.bicycle.colorgives"red", projections give arrays, and a path that does not match givesnullinstead of an error.
Use the one your platform already supports. JSONPath fits Kubernetes kubectl -o jsonpath, API test tools and anything that cites RFC 9535. jq fits shell scripts and CI. JMESPath fits the AWS and Azure CLIs. Try JMESPath expressions in the JMESPath tester, JSONPath in the JSONPath tester and jq filters in the jq tester.
Common gotchas and portability
- Filter parentheses differ. Goessner uses [?(@.x)], and RFC 9535 makes them optional as [?@.x]. Use the form with parentheses for the widest tool support.
- Existence is not truthiness. In RFC 9535, [?@.inStock] keeps every element that has an inStock member, including those where it is false. Write [?@.inStock == true] to test the value. Older JavaScript-based libraries evaluate the expression as JavaScript and treat false as a non-match, so the same query can return different results.
- Single value vs nodelist. Some libraries always return an array of matches, even when a path can match at most one node. Don't assume you get a scalar back.
- Result order can vary. Array elements keep index order, but JSON object members are unordered, so the order of matches across objects (especially with recursive descent) can differ between libraries.
- Use bracket quotes for keys that contain dots or spaces. $['user.name'] selects the literal key "user.name", while $.user.name goes down two levels.
- An empty result usually means the path did not match. It is not an error. Check that the document is valid with the JSON Validator before you assume the query is wrong.
If you are unsure, test the exact expression against the exact document. Small differences in slice bounds, filter operators or key escaping change the result. Pasting both into a live evaluator is the fastest way to find out.