Ultimate Guide
The Complete Guide to JSON Path (2026)
JSON Path is the shortest distance between a JSON document and the value you actually want. This guide covers everything a working developer needs — syntax, dot vs bracket notation, the RFC 9535 standard, JSONPath vs jq, and real code for JavaScript, Python, Postman, and Cypress. No fluff. Just the parts you'll use.
On this page
1. What is a JSON path?
A JSON path is a string expression that points to a specific value inside a JSON document. Think of it as an address: the root of the document is $, and each step deeper is either a key name (.user) or an array index ([0]).
Given this response from an API:
{
"user": {
"id": 42,
"name": "Ada Lovelace",
"emails": ["ada@example.com", "ada.l@work.io"],
"address": {
"city": "London",
"postcode": "NW1 6XE"
}
}
}
…the path to Ada's first email is $.user.emails[0], and the path to her city is $.user.address.city. That's the whole idea. Everything else in this guide is variations, edge cases, and language-specific glue.
RFC 9535 formalized JSONPath in early 2024. Before that, every library implemented a slightly different dialect. Most modern libraries now align with the RFC, but filter expressions and script blocks still have quirks — we'll flag them where they matter.
2. Dot vs bracket notation
JSONPath supports two ways to walk into an object. They're interchangeable most of the time, but each has situations where it's the only correct choice.
Dot notation (readable, but limited)
$.user.address.city // "London"
$.user.emails[0] // "ada@example.com"
$.user.id // 42
Dot notation only works for keys that are valid identifiers: letters, digits, underscores. If a key has a dash, a space, a dot, or starts with a number, dot notation breaks.
Bracket notation (verbose, but universal)
$['user']['address']['city']
$['user']['emails'][0]
$['x-request-id'] // dash key → brackets required
$['first name'] // space in key → brackets required
$['user'][0] // numeric key → brackets required
Bracket notation always works. It's slightly noisier, but if you're generating paths programmatically — say, from API responses you don't fully control — brackets are the safer default.
| Key looks like… | Use | Example |
|---|---|---|
userName | Either | $.userName or $['userName'] |
user-name | Bracket | $['user-name'] |
first name | Bracket | $['first name'] |
| Array element | Bracket | $.items[3] |
| Wildcard | Either | $.items[*] or $.items.* |
3. JSONPath expressions cheat sheet
These are the operators you'll actually use. Bookmark this section.
| Operator | Meaning | Example |
|---|---|---|
$ | Root of the document | $.user |
. | Child by name (dot notation) | $.user.name |
[...] | Child by name or index (bracket notation) | $['user'][0] |
* | Wildcard — all children | $.users[*].email |
.. | Recursive descent — any depth | $..email |
[start:end] | Array slice | $.items[0:3] |
[?(...)] | Filter expression | $.users[?(@.age > 18)] |
@ | The current node in a filter | $.orders[?(@.total > 100)] |
Filter expressions in one example
{
"orders": [
{ "id": 1, "total": 42, "status": "paid" },
{ "id": 2, "total": 199, "status": "paid" },
{ "id": 3, "total": 15, "status": "refunded" }
]
}
$.orders[?(@.status == 'paid' && @.total > 50)]
// → [ { "id": 2, "total": 199, "status": "paid" } ]
Filters are where JSONPath dialects diverge the most. Some libraries require single quotes, some require double. Some support && / ||, some only and / or. When in doubt, check your library's readme before writing 30 filters and wondering why nothing works.
4. Try it live: JSON Path Finder
Reading about paths only gets you so far. The fastest way to build intuition is to paste your own JSON and click around. That's exactly what JSON Path Finder does — click any value, get the exact path.
Open the tool in a new tab
No signup. No install. Handles large JSON without freezing your browser.
{
"user": {
"name": "Ada", ← click
"emails": [...]
}
}
Path: $.user.name ✓ copied
5. JSONPath vs JSONPointer vs jq
People conflate these three constantly. They solve overlapping problems, but they're not the same tool.
| Tool | Purpose | Sample syntax | Best when… |
|---|---|---|---|
| JSONPath | Locate values in JSON | $.users[?(@.age>18)].name |
You're inside code, tests, or a browser |
| JSONPointer | Point at a single node (RFC 6901) | /users/0/name |
You need an unambiguous, standard reference |
| jq | Transform and filter JSON on the CLI | .users[] | select(.age>18) | .name |
You're in a terminal, piping between commands |
Rough rule of thumb: JSONPath reads data, jq reshapes it, JSONPointer addresses it. If you find yourself building elaborate output structures, you're probably reaching for jq territory.
6. Using JSON paths in JavaScript
JavaScript has three tiers of "getting a value out of nested JSON," from native to full JSONPath library.
Tier 1 — Native optional chaining
const city = data?.user?.address?.city;
const firstEmail = data?.user?.emails?.[0];
Good enough for known, static paths. Zero dependencies. Breaks down when paths are dynamic strings.
Tier 2 — Lodash get()
import _ from 'lodash';
_.get(data, 'user.address.city');
_.get(data, ['user', 'emails', 0]);
_.get(data, 'user.address.zip', 'N/A'); // default value
Handy when the path itself is a string variable — for example, coming from a config file or a URL parameter.
Tier 3 — jsonpath-plus (real JSONPath)
import { JSONPath } from 'jsonpath-plus';
// All email addresses, no matter how deep
const emails = JSONPath({ path: '$..email', json: data });
// Every paid order with a total over $50
const bigPaidOrders = JSONPath({
path: "$.orders[?(@.status=='paid' && @.total>50)]",
json: data
});
Use jsonpath-plus the moment you need wildcards, recursive descent, or filters. It's small, maintained, and matches the RFC dialect closely.
7. Using JSON paths in Python
Python has a native dict-first approach, plus two mature JSONPath libraries. Pick based on what you need.
Native dict access
city = data["user"]["address"]["city"]
first_email = data["user"]["emails"][0]
# Safe version
city = data.get("user", {}).get("address", {}).get("city")
jsonpath-ng (recommended)
from jsonpath_ng.ext import parse
expr = parse("$.orders[?(@.status=='paid' & @.total>50)]")
matches = [m.value for m in expr.find(data)]
jsonpath-ng.ext gives you filter expressions and arithmetic. The plain jsonpath-ng module is stricter but faster on simple paths. For most projects, use .ext.
Piping into pandas
import pandas as pd
from jsonpath_ng.ext import parse
users = [m.value for m in parse("$.users[*]").find(data)]
df = pd.DataFrame(users)
Common pattern for data engineers: extract a "row-like" slice with JSONPath, then hand it to pandas.
8. Using JSON paths in Postman
Postman ships with Chai assertions and access to the response as a JavaScript object. Most Postman tests don't actually need a JSONPath library — you can walk the tree directly.
pm.test("User has valid email", function () {
const body = pm.response.json();
pm.expect(body.user.emails[0]).to.include('@');
});
When the response is deeply nested or paths are dynamic, add jsonpath-plus via the Postman sandbox:
const { JSONPath } = require('jsonpath-plus');
const body = pm.response.json();
const paidOrderIds = JSONPath({
path: "$.orders[?(@.status=='paid')].id",
json: body
});
pm.test("All paid orders returned", () => {
pm.expect(paidOrderIds.length).to.be.above(0);
});
9. Using JSON paths in Cypress & Playwright
Cypress
cy.request('/api/user/42')
.its('body.user.address.city')
.should('eq', 'London');
// For arrays / filters, drop into a callback
cy.request('/api/orders').then(({ body }) => {
const paid = body.orders.filter(o => o.status === 'paid');
expect(paid.length).to.be.greaterThan(0);
});
Playwright
const response = await request.get('/api/user/42');
const body = await response.json();
expect(body.user.address.city).toBe('London');
expect(body).toMatchObject({
user: { emails: expect.arrayContaining([expect.stringContaining('@')]) }
});
Both tools let you use plain JS access. Reach for JSONPath only when paths are dynamic or filters get gnarly.
10. Common mistakes & debugging
❌ Dot notation on a key with a dash
$.x-request-id is parsed as $.x minus request minus id. Use $['x-request-id'].
❌ Forgetting @ inside filters
Inside [?(...)], the current item is @, not $. Writing [?($.status == 'paid')] refers to the root's .status, not the item's.
❌ Assuming all libraries agree
$..book[(@.length-1)] works in some libraries and errors in others. If a path works in one language but not another, the dialect is the difference — not your JSON.
❌ Silent empty results
A path that matches nothing returns [], not an error. In tests, always assert the length is what you expect before asserting on values.
The fastest way to debug a bad path is to paste the JSON into the tool and click the value you actually want. If the click path differs from what you wrote, you have your answer.
Frequently Asked Questions
What is a JSON path?
$) followed by dot or bracket notation to walk from the root of the document down to a specific value. What is the difference between JSONPath and jq?
When should I use dot notation vs bracket notation?
$.user.name). Use bracket notation when a key contains spaces, dashes, reserved characters, or when you need to access an array element by index ($['user-name'], $.items[0]). Is JSONPath a formal standard?
Can I use JSONPath in Postman and Cypress?
pm.expect(pm.response.json()) with libraries like jsonpath-plus. Cypress commonly uses .its() with dot notation, but you can also load a JSONPath library for more advanced assertions. Playwright works the same way.