{ }jsonkitOpen

JSONPath cheat sheet: syntax, filters and examples

10 min read · updated 2026-10-01

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.jsonjson
{
  "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[*].title selects 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.

Filter examples against store.jsontext
$.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 object

Filters 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:

Equality, ranges, OR and filters after recursive descenttext
$.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/))].

Running a filter in JavaScript with jsonpath-plusjs
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.

ExpressionResult
$The entire document
$.store.booksThe 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[*].titleAll three titles
$.store.*The books array and the bicycle object
$..price[32.5, 42.0, 9.99, 199.95]
$..authorAll 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 TesterOpen the tool→

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.

TaskJSONPathjq
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 booksNot supported.store.books | length
Sum of pricesNot supported[.store.books[].price] | add
Sort by priceNot supported.store.books | sort_by(.price)
Rename fieldsNot supported.store.books[] | {name: .title, cost: .price}
Result shapeOne array of matchesA stream of values, one per line
The same three queries in jqbash
# 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

In 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.

OperationJSONPath (RFC 9535)jqJMESPath
Root$.Implicit. Expressions start at the root
Current node@ (inside filters).@
Child$.store.bicycle.color.store.bicycle.colorstore.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[].titlestore.books[*].title
Wildcard over object values$.store.*.store[]store.*
Recursive descent$..price.. | .price? // emptyNot supported
Filter$.store.books[?@.inStock == true].title.store.books[] | select(.inStock) | .titlestore.books[?inStock].title
Slice$.store.books[0:2].store.books[0:2]store.books[0:2]
Slice with step$.store.books[::2]No step syntaxstore.books[::2]
LengthOnly inside filters: $.store.books[?length(@.title) > 12].store.books | lengthlength(store.books)
Build new JSONNot 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 [?...]:

Books under $10 in each languagetext
# 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.color gives ["red"].
  • jq emits a stream of separate values. .store.books[].title prints three strings on three lines. Wrap the expression in [ ] to get one array.
  • JMESPath returns a single JSON value. store.bicycle.color gives "red", projections give arrays, and a path that does not match gives null instead 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.

Frequently asked questions

Is JSONPath a standard?

Yes, since February 2024. RFC 9535 specifies JSONPath's grammar and semantics, including how results are ordered and returned as a nodelist. Before that, libraries followed Stefan Goessner's original 2007 conventions, each with small variations.

What is the difference between JSONPath and jq?

JSONPath is a selection language that returns the values at matching locations in a document. jq is a full transformation language that can reshape, aggregate and build new JSON with pipes and functions. Use JSONPath to extract fields, and jq when the output structure differs from the input.

How do I select the last element of an array in JSONPath?

Use a negative index. $.items[-1] returns the last element. Negative indexing is part of RFC 9535, but some older pre-standard libraries may not support it.

Why does my filter [?(@.price<10)] return nothing?

There are three usual causes. The JSON is invalid (validate it first), the field name or path is wrong, or your library expects a specific filter form. Most tools want [?(@.price < 10)] with parentheses, and RFC 9535 also allows [?@.price < 10]. Test the exact expression in a live evaluator to find which one it is.

What is the difference between JSONPath, jq and JMESPath?

JSONPath (standardized as RFC 9535) selects values with paths that start at $ and returns a list of matches. jq is a full transformation language for the command line, with pipes and functions. JMESPath, used by the AWS and Azure CLIs, is a query language with projections, backtick literals and functions like length(), and it returns a single JSON value. Only JSONPath and jq have recursive descent (.. in both). Only jq and JMESPath can build new objects.

How do I filter by part of a string in JSONPath?

RFC 9535 has the search() function, as in $.store.books[?search(@.title, 'Code')]. Libraries that evaluate filters as JavaScript, including the jsonpath-plus library behind the JsonKit tester, use string methods instead, such as [?(@.title.includes('Code'))]. Check which form your library supports.

Can JSONPath modify or transform JSON?

No. JSONPath only selects. To edit values, compute aggregates or build new structures, use jq or your language's own JSON tools, such as JavaScript's map/filter or Python's json module.

What does the recursive descent operator (..) do?

It searches the current node and every descendant at any depth. For example, $..price finds every price field in the document however deeply it is nested. That makes it a good way to pull one field out of an irregular payload.