A little help goes a long way. Find a tool/

Developer FIELD NOTES · 4 MIN READ

How JSONPath Queries Work

JSONPath explained: root, dots, and brackets; wildcards, indexes, and slices; recursive descent with two dots; and what full JSONPath adds with filters.

JSONPath is to JSON what XPath is to XML: a tiny query language for pointing at nodes inside a document without writing loops. One expression like $..price can reach every price at any depth, replacing what would otherwise be nested for loops or a chain of map-and-filter calls.

The grammar: root, dots, brackets

Every JSONPath expression starts with $ — the root of the document. From there you descend with dots or brackets:

$.store.book[0].title             dot notation
$['store']['book'][0]['title']    bracket notation — same destination
$.store.book[*].author            [*] = every element of the array
$.store.book[-1]                  negative indexes count from the end

The two notations are interchangeable for simple names; brackets exist because keys can contain dots, spaces, or characters that would otherwise end the name — $['unit price'] works where $.unit price cannot. What a query returns is a list of matches, each with its own path and value: a wildcard over ten books yields ten results, not one merged object.

Wildcards and indexes

An index like [0] reaches into arrays, and wildcards reach into both arrays and objects — [*] on an object yields the values of all its keys. Wildcards are how you ask "all of them" without knowing how many exist: $.inventory[*].sku is the SKU of every item no matter how the catalog grows. Combined with exact indexes for the special cases ([0] first, [-1] last), this covers most lookups in one line.

Slices

Array sections use Python-style slices with up to three parts, [start:stop:step], where the stop index is exclusive:

$.store.book[0:2]     first two books (indexes 0 and 1)
$.store.book[1:3]     indexes 1 and 2
$.store.book[::2]     every second book
$.store.book[-2:]     the last two books

An empty part means "from the beginning", "to the end", or "step 1". Slices compose with everything else — $.store.book[0:2].title takes the titles of just the first two books.

Recursive descent: the two-dot superpower

.. descends through every level of the structure at once. Where a dotted path is an exact address, a descended path is a search. On the classic example document:

{
  "store": {
    "book":    [ { "price": 8.95 }, { "price": 12.99 } ],
    "bicycle": { "color": "red", "price": 19.95 }
  }
}

$..price              →  8.95, 12.99, 19.95   (every price, any depth)
$.store.bicycle.color →  "red"                (an exact address)

$..price finds prices wherever they hide — inside books, inside the store itself, inside nested objects you forgot existed. Bare $.. lists every node in the document, a quick way to dump a structure. The descent operator is the single most useful feature in day-to-day queries, because real documents bury the value you want at inconsistent depths.

Compared with plain JavaScript access

Why not just write data.store.book[0].title? For a fixed shape, plain access is faster and type-checks better. JSONPath earns its keep when the shape is not known at write time: the query is a string, so it can come from configuration, from a user, or from another program — which is exactly how extraction tools accept it. And several JSONPath selectors have no concise JavaScript equivalent: "every price at any depth" is a one-expression query, but the JavaScript version is a recursive walk over every object and array, with hasOwnProperty checks and careful null handling. The pattern-versus-code trade-off is the same one regular expressions make: write a string once, match everywhere.

What full JSONPath adds — and what to check first

The full specification goes further with filter expressions, such as $.store.book[?(@.price < 10)], which selects array elements by condition, and unions like ['a','b'] that grab several names in one bracket. Filters are the most-quoted advanced feature — and the least portable, since implementations disagree on details. A structural extractor keeps to the selectors above: names, indexes, wildcards, slices, and recursive descent, which cover the overwhelming majority of real-world lookups. Whatever tool you use, the input has to be valid JSON before any query can run — JSON Explained covers the syntax — and a validator that points at the exact broken line saves the guesswork.

Try it yourself

Paste a document and run queries like $..price or $.store.book[*].author in the JSONPath Extractor — it accepts dot and bracket notation, quoted names, indexes (including negative ones), wildcards, slices, and recursive descent, and shows every match with its full path. Validate the document first with the JSON Validator so a syntax error never masquerades as an empty result, and use the JSON Minify tool when you want the payload compacted for sharing or storage.

Frequently asked questions

What does the $ mean at the start of a JSONPath?
It is the root of the document — every expression begins there. $.store.book starts at the root, enters store, then book. From the root you descend with dots for simple names or brackets when the key contains dots, spaces, or other awkward characters.
What does $..price do?
The two dots are recursive descent: search every level of the structure for a member named price, no matter how deeply it is buried. A dotted path like $.store.bicycle.price is an exact address; $..price is a search that returns every match with its path.
How do slices work in JSONPath?
Python-style, with [start:stop:step] and an exclusive stop index. $.book[0:2] selects indexes 0 and 1, [::2] takes every second element, and negative values count from the end, so [-2:] means the last two items. Empty parts mean from the beginning or to the end.
Does JSONPath support filters like [?(@.price < 10)]?
The full specification does — filter expressions select array elements by condition, and they are the most-quoted advanced feature. They are also the least portable between implementations, while structural selectors (names, indexes, wildcards, slices, descent) behave consistently everywhere.
Why does my query return a list instead of a single value?
Because a JSONPath is a pattern, and the result is every node that matches. A wildcard over ten items yields ten results; even an exact address formally matches one node. Tools present the matches with their full paths so you can see where each came from.