If you've ever stared at a deeply nested API response and thought "I just need that one field buried in there" — JSONPath is the tool you're looking for. It's a query language for JSON, similar in spirit to how XPath works for XML. You write a path expression, you get back the matching values. No loops, no manual traversal. Stefan Goessner introduced it in 2007, and after years of slightly-incompatible implementations floating around, it was formally standardised as RFC 9535 in 2024. You can also explore JSONPath expressions interactively with the JSON Path tool.
The Dataset We'll Use Throughout
Rather than abstract examples, let's work with something realistic — an e-commerce order response. This is the kind of JSON you'd get back from an order management API:
{
"order": {
"id": "ORD-9182",
"status": "shipped",
"customer": {
"id": "CUST-441",
"name": "Maria Chen",
"email": "[email protected]"
},
"shippingAddress": {
"street": "14 Maple Avenue",
"city": "Portland",
"state": "OR",
"zip": "97201",
"country": "US"
},
"lineItems": [
{
"sku": "WH-1042",
"name": "Wireless Headphones",
"qty": 1,
"unitPrice": 89.99,
"inStock": true
},
{
"sku": "CB-USB-C",
"name": "USB-C Charging Cable",
"qty": 2,
"unitPrice": 12.49,
"inStock": true
},
{
"sku": "SC-PRO-7",
"name": "Phone Stand Pro",
"qty": 1,
"unitPrice": 34.00,
"inStock": false
}
],
"totals": {
"subtotal": 148.97,
"shipping": 5.99,
"tax": 11.92,
"total": 166.88
}
}
}The Root Operator $ and Basic Navigation
Every JSONPath expression starts with $, which refers to the root of the document.
From there, you navigate using dots. Want the order status? That's straightforward:
$.order.status
// → "shipped"
$.order.customer.name
// → "Maria Chen"
$.order.totals.total
// → 166.88
$.order.shippingAddress.city
// → "Portland"You can also use bracket notation, which is useful when a key contains spaces or special characters, or when you're accessing array elements by index. Both styles are valid according to RFC 9535's syntax rules:
// Dot notation
$.order.customer.email
// Bracket notation — equivalent
$['order']['customer']['email']
// Array index — first line item
$.order.lineItems[0].name
// → "Wireless Headphones"
// Last line item (negative indexing — supported in RFC 9535)
$.order.lineItems[-1].name
// → "Phone Stand Pro"$[-1:] (slice syntax) and
$.array[-1] (direct negative index) are both valid in RFC 9535, but some older
implementations don't support them. Test against your specific library if you're using negative indexes.Wildcards — Query All Children at Once
The * wildcard matches all elements at a given level. Instead of asking
for lineItems[0], lineItems[1], etc., you can grab everything at once:
// All line item names
$.order.lineItems[*].name
// → ["Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]
// All line item unit prices
$.order.lineItems[*].unitPrice
// → [89.99, 12.49, 34.00]
// All line item SKUs
$.order.lineItems[*].sku
// → ["WH-1042", "CB-USB-C", "SC-PRO-7"]This is the pattern you'll use most often when working with arrays in API responses. Instead of mapping over an array in your application code, you can extract exactly the field you need before it ever reaches your business logic.
Recursive Descent — Finding Values at Any Depth
The .. operator does a recursive search through the entire document tree.
It's like a depth-first walk that collects every node matching the key name, regardless of where it sits:
// Find every "name" field anywhere in the document
$..name
// → ["Maria Chen", "Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]
// Find every "city" field anywhere
$..city
// → ["Portland"]
// Find every field called "id" at any depth
$..id
// → ["ORD-9182", "CUST-441"]Notice how $..name finds both the customer name and all the product names —
it doesn't care about depth. This is powerful for schema exploration: when you're handed an
unfamiliar JSON blob and want to find all values for a particular key without knowing the structure.
If you want to sanity-check the JSON itself first, the JSON Validator
and JSON Formatter are handy starting points.
Array Slicing
JSONPath borrows Python-style slice notation for arrays. The format is
[start:end:step], where any part can be omitted.
This is documented in RFC 9535 § 2.3.5:
// First two line items
$.order.lineItems[0:2]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]
// From index 1 to end
$.order.lineItems[1:]
// → [{ sku: "CB-USB-C", ... }, { sku: "SC-PRO-7", ... }]
// Last item only (slice syntax — widely supported)
$.order.lineItems[-1:]
// → [{ sku: "SC-PRO-7", ... }]
// Every other item (step of 2)
$.order.lineItems[0::2]
// → [{ sku: "WH-1042", ... }, { sku: "SC-PRO-7", ... }]Filter Expressions — Querying by Condition
This is where JSONPath really earns its keep. The filter syntax [?(...)]
lets you query array items by their field values. The @ symbol refers to the current
item being tested. Stefan Goessner's
original 2007 article
introduced this syntax, and RFC 9535 formalized it:
// Line items under $50
$.order.lineItems[?(@.unitPrice < 50)]
// → [{ sku: "CB-USB-C", unitPrice: 12.49, ... }, { sku: "SC-PRO-7", unitPrice: 34.00, ... }]
// Only items currently in stock
$.order.lineItems[?(@.inStock == true)]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]
// Items where qty is greater than 1
$.order.lineItems[?(@.qty > 1)]
// → [{ sku: "CB-USB-C", qty: 2, ... }]
// Items that are NOT in stock
$.order.lineItems[?(@.inStock == false)].name
// → ["Phone Stand Pro"]You can combine conditions too. Most implementations support && and ||
inside filter expressions, letting you write things like
[?(@.inStock == true && @.unitPrice < 30)].
Check your library's docs — behaviour around operator support was one of the main inconsistencies
between pre-RFC implementations that the
RFC 9535 standardisation aimed to fix.
[?(@.fieldName)]. For example,
if some line items had a discountCode field and others didn't,
$.order.lineItems[?(@.discountCode)] would return only the discounted ones.Using JSONPath in JavaScript
There's no built-in JSONPath support in JavaScript yet (unlike XPath, which has
document.evaluate() in the browser). You'll need a library.
The two most popular are jsonpath
and jsonpath-plus.
The jsonpath-plus package is more actively maintained and has better RFC 9535 alignment:
npm install jsonpath-plusimport { JSONPath } from 'jsonpath-plus';
const order = {
order: {
id: 'ORD-9182',
status: 'shipped',
customer: { name: 'Maria Chen', email: '[email protected]' },
lineItems: [
{ sku: 'WH-1042', name: 'Wireless Headphones', qty: 1, unitPrice: 89.99, inStock: true },
{ sku: 'CB-USB-C', name: 'USB-C Charging Cable', qty: 2, unitPrice: 12.49, inStock: true },
{ sku: 'SC-PRO-7', name: 'Phone Stand Pro', qty: 1, unitPrice: 34.00, inStock: false }
],
totals: { subtotal: 148.97, shipping: 5.99, tax: 11.92, total: 166.88 }
}
};
// Get all product names
const names = JSONPath({ path: '$.order.lineItems[*].name', json: order });
console.log(names);
// [ 'Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro' ]
// Get in-stock items under $50
const affordable = JSONPath({ path: '$.order.lineItems[?(@.inStock == true && @.unitPrice < 50)]', json: order });
console.log(affordable.map(item => item.name));
// [ 'USB-C Charging Cable' ]
// Get the order total
const total = JSONPath({ path: '$.order.totals.total', json: order });
console.log(total[0]); // 166.88Note that JSONPath() always returns an array — even when you're querying a single value.
That's by design: a path expression is a selector, and a selector can match zero, one, or many nodes.
So always grab result[0] when you know you're targeting a unique value.
Using JSONPath in Python
In Python, the
jsonpath-ng
library is the most complete option. It supports the core spec plus most filter expression syntax:
pip install jsonpath-ngfrom jsonpath_ng import parse
order = {
"order": {
"id": "ORD-9182",
"status": "shipped",
"customer": {"name": "Maria Chen", "email": "[email protected]"},
"lineItems": [
{"sku": "WH-1042", "name": "Wireless Headphones", "qty": 1, "unitPrice": 89.99, "inStock": True},
{"sku": "CB-USB-C", "name": "USB-C Charging Cable", "qty": 2, "unitPrice": 12.49, "inStock": True},
{"sku": "SC-PRO-7", "name": "Phone Stand Pro", "qty": 1, "unitPrice": 34.00, "inStock": False}
],
"totals": {"subtotal": 148.97, "shipping": 5.99, "tax": 11.92, "total": 166.88}
}
}
# Parse the expression once, then reuse it (more efficient)
expr = parse("$.order.lineItems[*].name")
names = [match.value for match in expr.find(order)]
print(names)
# ['Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro']
# Get all line items and check stock
expr2 = parse("$.order.lineItems[*]")
for item in [m.value for m in expr2.find(order)]:
status = "in stock" if item["inStock"] else "OUT OF STOCK"
print(f"{item['name']} (${item['unitPrice']}) — {status}")
# Output:
# Wireless Headphones ($89.99) — in stock
# USB-C Charging Cable ($12.49) — in stock
# Phone Stand Pro ($34.0) — OUT OF STOCKThe parse() + find() pattern is the right approach for production use —
compile the expression once and run it against many documents, rather than re-parsing the path string each time.
The jsonpath-ng docs on GitHub
have more detail on extended filter syntax and the update() method for modifying matched nodes.
JSONPath vs jq — Which One Should You Reach For?
People sometimes conflate JSONPath and jq — they both "query JSON", but they solve different problems. Here's the practical breakdown:
- JSONPath is a path expression language designed to be embedded in applications. It queries and extracts values. You use it inside JavaScript, Python, Java code, or config files like Kubernetes selectors.
- jq is a full command-line processor with its own programming language. It can query, transform, reshape, filter, and compute new values. It's the right tool for shell scripts and one-off data wrangling in a terminal.
- Use JSONPath when you're writing application code that needs to extract fields from JSON programmatically — especially when the library needs to be embeddable and lightweight.
- Use jq when you're at a command line, writing shell scripts, debugging API responses, or need to do transformations (renaming fields, aggregating, building new structures) that go beyond simple extraction.
- jq syntax is more powerful but also more complex —
jq '.order.lineItems[] | select(.inStock) | .name'vs JSONPath's$.order.lineItems[?(@.inStock)].name. For pure extraction, JSONPath is often more readable.
There's also a middle ground: if you're already working in JavaScript and only need occasional
transformations, something like Lodash's _.get()
covers simple path access without any dependencies. But for anything involving wildcards, recursion,
or filter expressions, a proper JSONPath library is worth it.
Wrapping Up
JSONPath fills a real gap between "manually traverse this object in a for loop" and
"spin up a jq process". Once you get comfortable with $, ., [*],
.., and [?(filter)], you'll find yourself reaching for it constantly —
especially when working with large API payloads where you only need a handful of fields.
The RFC 9535 standardisation
means the syntax is now stable, so expressions you write today should work consistently across
compliant libraries.
Try your expressions out with the JSON Path tool — paste your JSON,
write a path, and see the results instantly. And if your JSON needs cleaning up first,
the JSON Formatter and JSON Validator
are right there too.