JSONPath Cheat Sheet

JSONPath selects values from a JSON document the way XPath selects nodes from XML. It is built into tools from Kubernetes kubectl to AWS Step Functions, Postman tests and many API gateways. This sheet covers the syntax and the portability traps.

A little history

Stefan Goessner described JSONPath in a 2007 article, and for seventeen years every library implemented its own reading of that page. In February 2024 the IETF published RFC 9535, a formal standard. Most syntax is shared, but filters, negative indexes and functions still differ between libraries, so it pays to know which one your tool uses.

All examples below use this document:

{
  "store": {
    "book": [
      { "title": "A", "price": 8, "isbn": "x" },
      { "title": "B", "price": 12 },
      { "title": "C", "price": 5, "isbn": "y" }
    ],
    "bicycle": { "color": "red", "price": 99 }
  }
}

Core syntax

  • $ — the root of the document; every path starts here
  • .store or ['store'] — a child by name; bracket form is required for names with spaces or special characters
  • .* or [*] — every child of an object or array
  • ..price — recursive descent: price at any depth
  • [0] — array element by index; [0,2] — several indexes (a union)
  • [1:3] — slice from index 1 up to, but not including, 3; [:2] from the start; [::2] every second element
  • [-1:] — the last element, using a negative slice start
  • @ — the current node, used inside filters
$.store.book[*].title          -> ["A", "B", "C"]
$..price                       -> [8, 12, 5, 99]
$.store.book[0,2].title        -> ["A", "C"]
$.store.book[:2].title         -> ["A", "B"]
$.store.book[-1:]              -> [{"title": "C", ...}]
$['store']['bicycle']['color'] -> ["red"]

A JSONPath query always returns a list of matches (a “nodelist”), even when exactly one value matches, and an empty list rather than an error when nothing matches.

Filter expressions

Filters keep the elements for which a condition is true:

$.store.book[?(@.price < 10)].title                  -> ["A", "C"]
$.store.book[?(@.isbn)].title                        -> ["A", "C"]   (key exists)
$.store.book[?(!@.isbn)].title                       -> ["B"]        (key missing)
$.store.book[?(@.title == "B" || @.price == 5)].title -> ["B", "C"]

Comparison operators are ==, !=, <, <=, > and >=; conditions combine with &&, || and !, and parentheses group them. Strings may be single- or double-quoted.

RFC 9535 writes filters without the outer parentheses, [?@.price < 10], and the parenthesised form is still valid under the RFC because parentheses are allowed around any logical expression. Many older libraries, however, only understand the parenthesised form. Write filters as [?(...)] for maximum portability.

Functions in RFC 9535

The standard defines five functions for use inside filters:

  • length(value) — length of a string, array or object
  • count(nodes) — number of nodes a sub-query returns
  • match(string, regex) — the whole string matches an I-Regexp pattern
  • search(string, regex) — the pattern matches somewhere in the string
  • value(nodes) — the single value of a sub-query, for comparisons
$.store.book[?length(@.title) > 1]
$.store.book[?search(@.title, '[Aa]')]

Older implementations do not have these; some instead allowed arbitrary JavaScript in filters, such as @.title.match(/a/i), which is unportable and, when evaluated with eval, a security risk. Libraries that support script filters usually offer a restricted “safe” evaluator.

Implementation differences to watch

  • Negative indexes. RFC 9535 defines [-1] as the last element. Several popular libraries return nothing for it; [-1:] works almost everywhere.
  • Filter syntax. As above: an unparenthesised filter may silently match nothing in an older library rather than raising an error. If a filter returns an empty list unexpectedly, try adding the parentheses.
  • Extensions. Some libraries add operators the RFC does not have. The widely used JavaScript library jsonpath-plus, for example, supports ^ for the parent of a match, ~ for property names ($.store.*~ gives ["book", "bicycle"]), and .length on arrays.
  • kubectl. Kubernetes uses its own JSONPath dialect inside templates: kubectl get pods -o jsonpath='{.items[*].metadata.name}'. The leading $ is optional, the expression sits in braces, and range/end loops are available.
  • Result shape. Some APIs return the list of matches; others return only the first match or unwrap single results.

PasteKit evaluates JSONPath with jsonpath-plus in its safe evaluation mode, locally in the browser, and preserves the exact digits of large numbers in the results. The parenthesised filter form is the one to use there.

Where you will meet JSONPath

  • Kubernetes: kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}' lists internal IPs, and --sort-by=.metadata.creationTimestamp takes a path too.
  • AWS Step Functions: InputPath, OutputPath and ResultPath use JSONPath such as $.detail to choose which part of the state a step sees and where its result is written.
  • API testing: Postman, REST Assured and many contract-testing tools assert on values selected with JSONPath, for example that $.data.items.length() or $.data.items[0].id has a given value.
  • Gateways and integration platforms: request mapping, routing on body fields and log redaction rules are often expressed as paths.

In each case the dialect is the tool’s own, so test a path against a real sample before relying on it in production configuration.

Debugging a path that returns nothing

An empty result is the most common JSONPath problem, and implementations rarely explain why. Work through it in order:

  1. Start from $ and add one segment at a time, checking the result after each. The step where the list becomes empty is where the path and the data disagree.
  2. Check the type at that step. .items.name selects nothing when items is an array; you need .items[*].name.
  3. Check key spelling and case. JSON keys are case-sensitive, and keys with hyphens or spaces need the bracket form ['content-type'].
  4. Check value types in filters. [?(@.id == 42)] does not match "id": "42", because the string and the number are different values.
  5. Try the parenthesised filter and the [-1:] slice if you used RFC-only syntax.

Pasting the document into the JSON formatter first helps more than it sounds: a tree view of the real structure usually shows the mismatch immediately.

JSONPath or jq?

JSONPath answers “where are the values?” and nothing more: it selects but cannot reshape, compute or construct new JSON. That makes it ideal for configuration fields that accept a path — API gateways, test assertions, Kubernetes output — and for quick lookups. When you need to transform the result, rename keys, aggregate or output CSV, switch to jq; the jq cheat sheet maps most of these selections to jq syntax ($..price is roughly [.. | .price? | numbers], and a filter becomes map(select(...))).

Frequently asked questions

Is JSONPath standardised?

Yes, since RFC 9535 was published in February 2024. Many libraries predate it and implement the original 2007 description with their own extensions.

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

Use [-1:], which works in almost every implementation. RFC 9535 also allows [-1], but some libraries return nothing for it.

Why does my JSONPath filter return an empty result?

Common causes are a filter written without parentheses for a library that needs [?(…)], comparing a number to a quoted string, or a key name with different case.

What does .. mean in JSONPath?

Recursive descent: $…price finds every price key at any depth of the document.

Related