A JSON document often holds far more than you need at any one moment. An order record contains the customer, the items, the payment details and the delivery address; the task in front of you might need only the product names, or only the orders over a certain value. Copying the whole document and picking through it by eye works once, and then it becomes slow and error-prone. JSONPath is the query language for this job: a short expression that points at exactly the values you want and returns just those.
The pieces of a JSONPath expression
Every JSONPath expression starts from the root of the document, written as $. From there you walk down through the structure, naming the fields and positions you want. JSON Studio includes JSONPath queries, and it runs entirely in your browser, so the document you query is never uploaded.
Dot notation for named fields. $.customer.name means: start at the root, go into the customer object, and take its name field. Dot notation is the form you will use most, because most JSON navigation is simply naming the next field down. It reads almost like a sentence, which is why a query stays understandable weeks later.
Bracket notation for awkward keys. When a key contains a space, a dot or a dash, use brackets with quotes instead: $["customer name"]. Bracket notation also works for ordinary keys, so $["customer"]["name"] means the same as $.customer.name.
Array indexes. Arrays are addressed by position, starting from zero. $.items[0] is the first item, $.items[1] the second. A negative index counts from the end, so $.items[-1] is the last item — handy when you want the most recent entry and do not know how long the list is.
Wildcards. The wildcard * means "every child at this level". $.items[*].product returns the product name of every item, and $.* returns every top-level value. Wildcards are the quickest way to flatten one level of structure into a list of values.
Recursive descent. Two dots, .., search at every depth below the current point. $..price finds every price field anywhere in the document, however deeply nested. It is powerful and should be used sparingly: on a large document it can return values from places you did not expect, such as a nested history record that also happens to have a price field. Prefer a precise path when you know the structure.
Filters. A filter selects array items that meet a condition, written in brackets with a ? and an @ standing for the current item: $.items[?(@.price > 20)] returns only the items priced above 20.
Worked example: one order, five questions
Here is the document we will query — an order with a customer, three items and a delivery record:
{
"order_id": 4821,
"status": "shipped",
"customer": {"name": "Mara Ellison", "email": "mara@example.com", "city": "Leeds"},
"items": [
{"product": "linen apron", "price": 27, "qty": 2},
{"product": "sewing kit", "price": 18, "qty": 1},
{"product": "fabric scissors", "price": 14, "qty": 3}
],
"delivery": {"method": "standard", "cost": 4}
}
Paste it into JSON Studio and try each query in turn:
- Who placed the order?
$.customer.namereturns"Mara Ellison". - What was ordered?
$.items[*].productreturns["linen apron", "sewing kit", "fabric scissors"]— every product name, nothing else. - Which items cost more than 20?
$.items[?(@.price > 20)].productreturns["linen apron"]. The filter runs over each item, keeps the ones whose price passes the test, and then takes the product name from each survivor. - What is the last item?
$.items[-1].productreturns"fabric scissors", without needing to know the list has three entries. - What is the total quantity? JSONPath selects values; it does not calculate with them.
$.items[*].qtyreturns[2, 1, 3], and adding them up is a step you do in your own code or by hand. Knowing this boundary saves confusion: if a query seems unable to compute something, aggregation is simply outside what JSONPath does.
Notice how each query returns a smaller, cleaner result than the document it came from. That is the point. Instead of handing a colleague or a script the whole order, you hand them the exact list they asked for.
Common query patterns to keep
All values of one field, at any depth. $..email finds every email field in the document. Useful for a quick audit, but verify the results, since a nested record may hold an older address you did not mean to include.
One field from every object in an array. $.items[*].price is the pattern behind most "give me a column" requests: a list of prices, names or dates. Pair it with a filter when you only want some rows: $.items[?(@.qty > 1)].product gives the products ordered more than once.
Items matching a value exactly. $.items[?(@.product == "sewing kit")] returns the full sewing-kit entry. Equality filters are the most reliable kind, because they do not depend on numeric comparison or string ordering behaving the way you expect.
Combining conditions. $.items[?(@.price > 15 && @.qty > 1)] returns items that are both above a price and ordered in quantity. Keep combined filters to two conditions where possible; beyond that, run one filter, inspect the result, and then refine it.
First, last, and slices. $.items[0] takes the first item, $.items[-1] the last, and $.items[0:2] takes a slice — the first two items, since the end index is exclusive. Slices are useful for previews: the first few entries of a long list, to check the shape before querying the whole thing.
Checking a field exists. $.items[?(@.discount)] returns items that have a discount field at all, regardless of its value. This is a quick way to find which records carry an optional field and which do not.
When a query returns nothing
An empty result usually means one of a few things, and they are worth checking in order. First, spelling and case: JSON keys are case-sensitive, so $.Customer will not match customer. Second, structure: if items is nested inside another object, a query starting $.items will miss it — open the tree view and confirm the path matches the real nesting. Third, type: a filter comparing @.price > 20 will not match a price stored as the string "27", because a string is not a number. Fourth, arrays versus objects: an expression written for an array returns nothing when applied to an object, and vice versa. Finally, JSONPath implementations differ slightly in edge cases — filter syntax and slice behaviour are the usual places — so test a query against a small sample before running it over a large document.
For the companion workflow of checking a document before you query it, see the guide on how to format and validate JSON.
Honest limitations
JSONPath selects values; it does not validate structure or syntax, and it does not judge whether the data means what you intended. A query that returns a price of 240 when you expected 24 has done its job correctly — the error is in the data, not in the query. JSONPath also cannot transform data: it cannot rename fields, reformat dates, or sum a column, so any reshaping still happens after the query, in your own code. Browser file-size limits apply as well: very large documents can slow a browser tab, so query a section of a huge file rather than the whole thing where possible.
FAQs
What is the difference between $.items and $.items[*]?
$.items returns the whole array as one value. $.items[*] returns every element of the array, as separate results. Use the first when you want the list itself, and the second when you want to work with the entries inside it, or pull one field from each.
Why does my filter return nothing when matching items exist?
Check three things first: the key spelling and case, whether the value's type matches the comparison (a string "27" is not the number 27), and whether the path to the array matches the real nesting. One of those three explains most empty results.
Can JSONPath add up numbers or count items?
No. JSONPath selects and filters values; it does not aggregate them. $.items[*].qty gives you the list of quantities, and the summing happens in your own code or by hand afterwards.
When should I use recursive descent (..) instead of a precise path?
Use it to explore a document whose structure you do not know, or to audit every occurrence of a field. For repeated queries, prefer the precise path: it returns fewer surprises from nested records and breaks visibly if the structure changes.
If a query result needs a pattern check — for instance, verifying that every returned value matches an expected format — the Regex Tester can test that pattern, and if you need to compare two text results side by side, Text Compare handles that comparison.
human review pending client designation