JSONPath: Syntax, Examples and Finding the Path to a Value
An API response arrives three hundred lines long and somewhere inside it sits the one field you need: a price, an identifier, an order status. Reading such a document by eye is awkward and unreliable, because half the lines are nested objects and arrays, it is easy to miss a second occurrence of the same key or to lose track of which level a field lives on. JSONPath was invented for exactly this: a small language in which the route to a value is written as one line. Here is how it works, where its limits are, and how to apply it to a real API response.
What JSONPath is and why it exists
JSONPath describes not the data itself but the route to it. A path starts at the root of the document, written as the dollar sign, and then continues in steps: a key name, an array index, sometimes a condition. The idea is old and obvious: if XML addresses a node as /order/items/item[3]/price, then JSON needs a way of saying "take the third element of the items array and show me its price field".
- it works the same with objects, arrays and structures nested to any depth;
- a path reads almost like a sentence: $.orders[0].price means "at the root, the orders array, the zeroth element, the price field";
- the result is a list rather than a single value, because the same key may appear in several branches of the document;
- the expressions are understood by libraries across languages: Python, JavaScript, Java, Go, PHP;
- JSONPath answers "where does this value live", not "what to do with it": it is not a programming language and not a replacement for JSON.
One boundary is worth stating up front, because there is a lot of confusion around the name. JSONPath is not the only query language for JSON and it is not one single standard. There is jq with a filter language of its own, there is JMESPath with a different grammar, and there is SQL/JSON Path from the SQL standard. The names look alike, the grammars are not, and mixing them inside one project is a good way to waste an afternoon.
JQL, the query language of Jira, is worth mentioning too, because people often offer it as an example of JSONPath. It looks nothing like it: project = PROJ AND status = Open, closer to SQL, where a condition is spelled out in words rather than symbols. JMESPath is closer to JSONPath in spirit, but its filters are written differently, with backticks around the value: items[?price > `10`]. All three languages solve one problem, but an expression should never be moved from one of them to another without checking.
Basic syntax: root, dot and brackets
The base language has few constructs, and they cover most real tasks. The full list is short.
- $ is the root of the document, the starting point of every path;
- .key reaches a field of an object: $.user.name;
- ["key with spaces"] does the same for keys that cannot be written with a dot: $["order id"];
- [0] picks an array element by index, counting from zero: $.orders[0].price is the first order;
- [*] picks every element of an array: $.orders[*].price;
- .* picks every value of an object at the current level: $.user.*;
- ..key is the recursive descent, covered in detail below.
The two ways of writing the same key are equivalent: $.user.name and $('user')['name'] point at one and the same field. The difference is only in readability and in which characters you have to escape. Once a key contains a space, a dash or a dot, the dot form stops working and the bracket form is the only option.
Mixing the two syntaxes explains a good share of the mistakes. In JavaScript a field of an object is reached with a dot and an element of an array with an index in square brackets, with no prefix at all: obj.user and arr[0]. JSONPath adds the dollar sign at the front and turns access to a key into a step of its own. Hence a simple rule: a path that starts with $ is JSONPath, while an expression without that prefix is ordinary code and has to be written in a programming language.
Recursive descent and searching by value
The double dot saves more time than anything else in the language. The expression $.name means "a field called name at the top level", while $..name means "a field called name at any depth where it occurs". Recursive descent is what turns JSONPath from precise addressing into a practical search tool for an unfamiliar structure.
{
"meta": { "page": 1, "total": 137 },
"data": [
{ "id": 1024, "price": 7490.5, "seller": { "id": 55, "name": "Acme Trading" } },
{ "id": 1025, "price": 3290.0, "seller": { "id": 55, "name": "Acme Trading" } }
]
}
$.data[0].price → 7490.5 exact path, branch is known
$.data[*].price → 7490.5, 3290.0 every element of the array
$..price → 7490.5, 3290.0 the same thing, recursively
$..id → 1024, 1025, 55, 55 every id, nested ones included
(traversal order depends on the implementation)
$..name → "Acme Trading", "Acme Trading"
$.meta.total → 137
What JSONPath cannot do: find an element by the value of a field.
The question "which item has price 3290" needs a filter, not a base path.The difference between searching by name and searching by value matters in practice. The expression $..price finds every field called price, but it does not answer which product costs 3290. A path built from names never inspects the contents of the values at all.
Filters and slices: what they really are
Most examples on the internet use constructs such as $.items[?(@.price < 10)], which selects the items whose price field is below 10, and $.items[0:3], which takes the first three elements. These were not part of the original JSONPath. They come from extensions that drifted apart between libraries, every implementation interprets them differently, and strictly speaking only the base part of the language is standardised.
- the base JSONPath is guaranteed to handle $, ., [] and the recursive descent ..;
- filters written as ?() differ between libraries: some offer comparison operators and logical connectors, some support plain equality only;
- slices of the form [start:end] are present in some libraries and missing in others;
- the output of a filter is always a list, even when exactly one element matches;
- a specific expression can only be verified in the library that will actually run it.
Now the path finder on Toolbox Pro, so there are no misunderstandings. It scans the document for a key or a value that contains your search string and shows the path to every match: $.data[0].price, $.user.address.city. That is genuinely useful at the first step, when the shape of the response is still unknown. What it does not do is evaluate expressions: a query such as $.items[?(@.price < 10)] returns no matches, because the search looks for a substring inside keys and values rather than computing anything. For computed queries you need a library for your language.
Two related tools help around it. The JSON formatter points out syntax errors and lays the document out so a human can read it, and if you need to understand the shape of a response well enough to write types for it, the TypeScript type generator turns the sample into an interface.
Walking through a real API response
Take a typical catalogue response and go from "I have no idea where the price is" to a finished path you can copy into code.
- paste the response into the input area and press Format so the JSON spreads into a readable shape;
- type the key name into the search box, for example price;
- the "search keys" and "search values" switches decide where the substring comparison happens;
- collect the list of paths such as $.data[0].price and $.data[1].price, each shown with its value and type;
- copy the path you need with the button next to it;
- use that path in Postman, in an automated test or in application code.
Response:
{
"meta": { "page": 1, "per_page": 20, "total": 137 },
"data": [
{ "id": 1024, "title": "Running shoes", "price": 7490.5, "stock": 12,
"order-id": "A-77", "seller": { "id": 55, "name": "Acme Trading" } },
{ "id": 1025, "title": "Backpack", "price": 3290.0, "stock": 0,
"order-id": "A-78", "seller": { "id": 55, "name": "Acme Trading" } }
]
}
Query "price":
$.data[0].price number 7490.5
$.data[1].price number 3290
Query "total":
$.meta.total number 137
Query "Acme" (searching by value):
$.data[0].seller.name string Acme Trading
$.data[1].seller.name string Acme Trading
Query "stock":
$.data[0].stock number 12
$.data[1].stock number 0
A key containing a dash has to be written with brackets:
$["data"][0]["order-id"] string A-77Look at the last line of the example. The key order-id contains a dash, so the dotted form $.data[0].order-id does not work, because the dash reads as a minus sign. The correct path is $["data"][0]["order-id"]. These small details are what break a test in a pipeline: the path looks right and the field is simply not found.
Finding several paths for one query is not an error but a sign that the key occurs in different branches of the document. Sorting them out takes context, because two paths with the same ending may point at different entities. $.data[0].seller.id belongs to the seller, while $.meta.owner.id belongs to the owner of the catalogue, and confusing the two is dangerous. Recursive descent helps here: $..id collects every match at once, and the values themselves tell the two apart.
Common syntax mistakes
- mismatched quote styles: a key inside brackets needs quotes, and the quote type has to match the type of the outer string, otherwise you end up with two sets of quotes in a row;
- using a dot where brackets are required for a key with a space or a dash: $.order id and $.order-id do not work, $['order id'] does;
- confusing an index with a key: $.orders[0] is the first array element and $.orders["0"] is the same thing, while $.orders.id does not exist at all;
- forgetting that indexing starts at zero: $.orders[1] is the second order, not the first;
- mixing JSONPath with jq: in jq a filter is written .data[] | .price rather than $.data[*].price, and these are two separate languages;
- expecting $..price to return a single value when it returns a list, so the value arrives as an array that still has to be unpacked;
- trying to write a condition in the base syntax: $.price > 1000 finds nothing, because comparison operators are not part of the base language.
Another mistake lives outside the syntax: using a path to a single array element where the whole array was meant. $.data[0].price gives the price of the first product only, while $.data[*].price gives the price of every one of them. A check of the form "every product has a price" needs the second form, and a check of the form "the first product costs more than a thousand" needs the first.
Applying the path in Postman and in automated tests
In Postman the response body is turned into a JavaScript object, so scripts use direct property access rather than JSONPath. The path you found works perfectly well as a reference: $.data[0].price becomes body.data[0].price.
const body = pm.response.json();
pm.test("the first product costs more than 1000", function () {
pm.expect(body.data[0].price).to.be.above(1000);
});
pm.test("all ids are numbers", function () {
const ids = body.data.map(function (item) { return item.id; });
pm.expect(ids).to.eql([1024, 1025]);
});
pm.test("at least one product is in stock", function () {
const inStock = body.data.filter(function (i) { return i.stock > 0; });
pm.expect(inStock.length).to.be.above(0);
});In ordinary automated tests, on the other hand, JSONPath works natively, because there you call a library yourself.
# Python, jsonpath-ng library
from jsonpath_ng.ext import parse
expr = parse("$.data[*].price")
prices = [float(m.value) for m in expr.find(body)]
assert prices == [7490.5, 3290.0]
# Java, RestAssured
response.jsonPath().getList("data.price", Float.class)
# → [7490.5, 3290.0]
# JavaScript, jsonpath-plus library
const prices = JSONPath({ path: "$.data[*].price", json: body });
# → [7490.5, 3290.0]
# jq on the command line, with its own syntax
jq '.data[].price' response.jsonBefore you pin a path down in code, it is worth comparing the responses of two versions of an API. The JSON diff tool shows the differences between two documents, and if the response has to satisfy a contract, the JSON Schema validator checks it against the schema.
The same path is useful outside Postman as well. In the browser console on an application page, response data often sits in some variable, and the path you found points you at the right place. On the command line jq does the same job with its own syntax: jq .data[].price for an array and jq -r .meta.total for a single value. The -r flag strips the quotes around strings, without it text is printed as a piece of JSON rather than as a finished value.
Common questions
How is JSONPath different from XPath?
XPath works on XML and uses separators and axes: /store/book[1]/title, bookstore/book/@isbn. JSONPath is simpler, because the shape of JSON is known in advance: objects have field names, arrays have indices. That gives a different notation, different numbering and different behaviour when an element is missing. XPath returns an empty result, while JSONPath implementations either return an empty list or raise an error.
What do $ and .. mean?
The dollar sign is the root of the document, the starting point of every path, much like the root of a tree. The double dot means recursive descent, a move to any depth below. So $.name looks for a field called name at the top level only, while $..name finds every field called name in the document, including those buried inside arrays and nested objects.
How do I select every element of an array?
Use the asterisk inside brackets: $.data[*].price. The asterisk means "every element of the data array", and the rest of the path is applied to each of them. If you want the whole array without picking a field, write $.data[*]. One caveat: some implementations return a list of lists for $.data[*], and the extra brackets have to be stripped in code.
Do all libraries support filters?
No, and that is the main reason to test a filter in the environment where it will actually run. The base part of the language, with $, dots, brackets and recursive descent, is supported everywhere, while filters written as ?(), slices and operations on values came from extensions whose coverage differs from library to library. Code that passes on one machine fails on another for no better reason.
Can I find a path without knowing the structure of the JSON?
Yes, and that is the most common scenario. Searching for a substring inside key names and values returns every matching place together with a ready-made path, and what remains is picking the right one. That is how you locate an identifier, a price or a supplier name in a response whose structure you are seeing for the first time. One thing to keep in mind is that the output is always a list: if the key occurs in three branches, you get three paths and you choose by hand.
What should I do when a path finds nothing?
First check that the document parses at all: an unbalanced brace or a trailing comma breaks the whole JSON, and searching inside it is pointless. Then try the recursive descent $..key, which finds a field at any depth and does not depend on which level you expected it at. And compare the spelling: a substring search ignores case, but paths in some libraries are case sensitive, so the way a key is written in the document and in the expression has to match.