Skip to main content
CuteJSON
↑↓ Navigate Enter Select Esc Close

JSONPath cheat sheet, with worked examples

Published 4 min read By Akash Kinkar Pandey

JSONPath is a query language for JSON, in the way that XPath is one for XML. It was sketched by Stefan Goessner in 2007, implemented differently by everyone who needed it, and finally standardised as RFC 9535 in 2024. That history is why two JSONPath implementations often disagree, and why a cheat sheet is more useful with results attached than as a list of symbols.

Every query below runs against this document. You can paste it into the JSONPath tester, which loads exactly this as its sample.

{
  "store": {
    "book": [
      { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 },
      { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 },
      { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "expensive": 10
}

The selectors

Root. $ is the whole document. Every query starts there.

Child by name. $.store.bicycle.color returns "red". A dot then a name, as many times as you need.

Quoted name. $['store']['bicycle'] is the same thing written the other way. You need the brackets when a key has a space, a dot or a dash in it, or when it starts with a digit: $['first name'].

Wildcard. $.store.bicycle.* returns "red" and 19.95, every child of that object. $.store.book[*] returns all three books.

Index. $.store.book[0] is the first book. $.store.book[-1] is the last one, counting back from the end.

Slice. $.store.book[0:2] returns the first two books. The end is exclusive, exactly as in Python. $.store.book[::2] takes every second book, and a negative step reverses: $.store.book[::-1].

Union. $.store.book[0, 2] returns the first and third books. $.store.bicycle['color', 'price'] selects two named children at once.

Descendant. $..price returns every price at any depth, which here is 8.95, 12.99, 8.99 and 19.95. This is the selector people reach for most and the one that costs the most on a large document, because it visits every value.

Filter. $.store.book[?@.price < 10] returns the two cheaper books. The @ refers to the value being tested. RFC 9535 dropped the parentheses that older tutorials use, so both [?@.price < 10] and [?(@.price < 10)] appear in the wild. The tester here accepts both.

Existence. $.store.book[?@.isbn] returns only the book that has an isbn field, without comparing it to anything.

Combining tests. &&, || and ! work as you would expect: $..book[?@.category == 'fiction' && @.price < 10].title returns "Moby Dick".

Why a filter that looks right returns nothing

Nearly always, it is a type mismatch. The specification says values of different types are never equal and never ordered, so a price stored as "8.95" with quotes does not match @.price < 10, and it does not match @.price == 8.95 either. If a filter returns nothing, check the types in the document before you check the query.

The second cause is attaching the filter to the wrong level. A filter picks from among the children of the value it is attached to, so it goes after the array: book[?@.price < 10], not book.price[?...]. Attached to an object, it tests each property value of that object instead.

Third, a missing value is not null. @.isbn == null matches a book whose isbn is literally null, not a book that has no isbn at all. To test for absence, write !@.isbn.

Where implementations disagree

The 2007 article was a proposal, not a specification, so libraries diverged for seventeen years. If you are moving a query between tools, these are the places to check:

  • Negative indexes and negative steps are widely supported now and missing from some older libraries.
  • The parenthesised filter form is the old spelling. RFC 9535 uses bare [?...], and plenty of tools accept only one of the two.
  • The RFC defines the functions length(), count(), match() and search(). Many implementations either lack them or ship a different set under different names.
  • The RFC gives results a document order. Some implementations do not guarantee it, particularly for descendant segments.

The tester on this site implements the selectors listed above and says so plainly on the page, including a table of what it supports. Function extensions, absolute paths inside a filter, and comparing two paths against each other are not implemented, and using one of them produces a message naming the feature rather than a syntax error. That is a deliberate choice: a tool that claims full compliance and then fails quietly on an edge case is worse than one that tells you where its edge is.

JSONPath or JSON Pointer

They answer different questions.

A JSON Pointer names exactly one value and either resolves or does not. It has no wildcards and no filters, which is precisely why JSON Schema, JSON Patch and most validation error messages use it: an unambiguous address is what those need. /store/book/0/title means one place.

A JSONPath query is a search. It can return many values or none, and it can filter. Use it when you do not already know where the thing you want is.

If you know the address, use a pointer. If you are looking for something, use a query. Both tools on this site report the line each result sits on, so either way you end up back in the document rather than in an abstract result list.

Back to all posts