JSONPath examples in JavaScript, Python, PostgreSQL and kubectl
· 6 min read · By GigaJSON
JSONPath has had a standard since February 2024, RFC 9535. The library in front of you still decides what works. The same filter can return two titles in one tool, an empty list in another, and a parse error in a third.
So here are the same tasks in five places, with the output each one gave me:
- JavaScript: jsonpath-plus 11.1.0
- Python: jsonpath-ng 1.12.0 and python-jsonpath 2.2.2
- PostgreSQL 17.11
- kubectl (client-go's jsonpath package, the engine
kubectl -o jsonpathruns)
The JSONPath Tester runs jsonpath-plus on the same data, so you can paste any JavaScript example there and change it.
The sample data
A small bookstore, the data the tester starts with:
{"store": {"name": "Corner Books", "books": [
{"title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95, "tags": ["quotes"]},
{"title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99, "tags": ["fiction", "war"]},
{"title": "Moby Dick", "author": "Herman Melville", "price": 8.99, "tags": ["fiction", "classic"]},
{"title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99, "tags": ["fantasy"]}
]}}
Paths that work everywhere
These gave the same result in jsonpath-plus, jsonpath-ng (its ext parser for the filter) and python-jsonpath. A library returns a list of matches even when there's one.
| Task | JSONPath | Result |
|---|---|---|
| Every title | $.store.books[*].title |
all four titles |
| The first book | $.store.books[0].title |
["Sayings of the Century"] |
| The first two | $.store.books[0:2].title |
Sayings of the Century, Sword of Honour |
| The last book | $.store.books[-1:].title |
["The Lord of the Rings"] |
| Every price, at any depth | $..price |
[8.95, 12.99, 8.99, 22.99] |
| Books under 10 | $.store.books[?(@.price < 10)].title |
Sayings of the Century, Moby Dick |
Two details already differ. [-1] without the colon gave the last book in both Python libraries and an empty list in jsonpath-plus; the slice [-1:] works in all three. And the filter without parentheses, [?@.price < 10], is the RFC 9535 form: both Python libraries accept it, while jsonpath-plus returns an empty list without an error.
Filters: where the libraries part ways
Each cell is the expression that returned the right books in that library.
| Task | jsonpath-plus | jsonpath-ng (ext) | python-jsonpath |
|---|---|---|---|
| Under 10 and fiction | [?(@.price < 10 && @.tags.includes('fiction'))] |
[?(@.price < 10 & @.tags[?@ == 'fiction'])] |
[?@.price < 10 && @.tags[?@ == 'fiction']] |
| Tagged fiction | [?(@.tags.includes('fiction'))] |
[?@.tags[?@ == 'fiction']] |
[?@.tags[?@ == 'fiction']] |
| Author matches a pattern | [?(@.author.match(/melville/i))] |
[?(@.author =~ 'Melville')] |
[?search(@.author, 'Melville')] |
| More than one tag | [?(@.tags.length > 1)] |
[?(@.tags.`len` > 1)] |
[?length(@.tags) > 1] |
| Count the books | $.store.books.length |
$.store.books.`len` |
only inside filters |
The pattern is easy to remember once you see why. jsonpath-plus runs the filter as a small piece of JavaScript, so you get JavaScript's &&, .includes(), .match() and .length. python-jsonpath follows RFC 9535, whose filters use nested selectors and the functions length(), count(), match(), search() and value(). jsonpath-ng's extensions came before the RFC and have their own operators.
JavaScript: jsonpath-plus
import { JSONPath } from 'jsonpath-plus'
JSONPath({ path: '$.store.books[?(@.price < 10)].title', json: data })
// [ 'Sayings of the Century', 'Moby Dick' ]
By default the filter code runs in jsonpath-plus's own safe evaluator (eval: 'safe'), not JavaScript's eval, so a query typed by a user can't run arbitrary code. A query in another library's syntax often returns [] instead of an error, so test the exact string you'll ship.
Python: jsonpath-ng and python-jsonpath
jsonpath-ng is the most used, but filters need its extended parser. The base parse stops at the ?:
from jsonpath_ng import parse # no filters
from jsonpath_ng.ext import parse # filters, `len`, =~
[m.value for m in parse("$.store.books[?(@.price < 10)].title").find(data)]
# ['Sayings of the Century', 'Moby Dick']
With the base parser the same line fails with JsonPathLexerError: Error on line 1, col 14: Unexpected character: ?. Between conditions jsonpath-ng uses a single &; && is a parse error.
python-jsonpath tests against the JSONPath Compliance Test Suite, so its syntax is the RFC's:
import jsonpath
jsonpath.findall("$.store.books[?search(@.author, 'Melville')].title", data)
# ['Moby Dick']
If you're starting fresh in Python, I'd pick python-jsonpath: the queries you write will also work in other RFC 9535 libraries.
PostgreSQL: SQL/JSON path
PostgreSQL's jsonb type has its own path language from the SQL standard. It starts with $ too, but filters come after the list with ?, and strings take double quotes:
SELECT jsonb_path_query_array(doc, '$.store.books[*] ? (@.price < 10).title')
FROM shop;
-- ["Sayings of the Century", "Moby Dick"]
The same tasks in PostgreSQL's syntax:
| Task | SQL/JSON path |
|---|---|
| Under 10 and fiction | $.store.books[*] ? (@.price < 10 && @.tags[*] == "fiction").title |
| Tagged fiction | $.store.books[*] ? (@.tags[*] == "fiction").title |
| Author matches, any case | $.store.books[*] ? (@.author like_regex "melville" flag "i").title |
| Title starts with S | $.store.books[*] ? (@.title starts with "S").title |
| The last book | $.store.books[last].title |
| The first two | $.store.books[0 to 1].title |
| Count the books | $.store.books.size() |
Three things caught me out:
- Single quotes inside the path,
@.author == ''Herman Melville'', givesyntax error at or near " " of jsonpath input. Strings in a path are always in double quotes. - Lax mode, the default, unwraps lists for you, so
$.store.books.titleworks without[*]. Addstrictat the start and the same path fails withjsonpath member accessor can only be applied to an object. $.**.pricein lax mode returned all four prices twice, eight values.strict $.**.pricereturns each one once.
To compare against a value from your application, pass it as a variable instead of building the string: jsonb_path_query_array(doc, '$.store.books[*] ? (@.price < $max).title', '{"max": 10}').
To find rows, use @? (does the path match anything?) or @@ (is the condition true?) in the WHERE clause. A GIN index with jsonb_path_ops makes them fast for equality checks. On a test table of 20,000 rows, doc @? '$.store.books[*] ? (@.price == 99)' used the index:
CREATE INDEX ON shop USING gin (doc jsonb_path_ops);
SELECT id FROM shop WHERE doc @? '$.store.books[*] ? (@.price == 99)';
kubectl: -o jsonpath
kubectl uses JSONPath as a template language. Each path sits in { }, and the text between them is printed as is. These ran on a list of two pods:
kubectl get pods -o jsonpath='{.items[*].metadata.name}'
# web-1 db-1
kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'
# nginx:1.27 busybox:1.36 postgres:17
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
# web-1 node-a
# db-1 node-b
kubectl get pods -o jsonpath='{.items[?(@.metadata.labels.app=="web")].metadata.name}'
# web-1
What's different here:
{range .items[*]} ... {end}prints one line per item. Without it, all the values of a path come out on one line, separated by spaces.- A filter takes one condition.
&&is a parse error:unrecognized character in action: U+0026 '&'. For more, pipe-o jsonto jq. - Dots in a key must be escaped, even in brackets:
{.metadata.labels.app\.kubernetes\.io/name}or{.metadata.labels['app\.kubernetes\.io/name']}. Unescaped, kubectl readsappas the key. [-1]works here and gives the last item.- There's no
.length. Count withkubectl get pods --no-headers | wc -linstead.
Which syntax to write
Write for the engine that will run the query, and test it there. The same text can be valid in two libraries and mean different things, or be wrong in one and come back empty instead of failing.
To get the path to a value without writing it, paste your JSON into the JSON Path Finder and click the value: it gives the path as JSONPath, jq and JavaScript. To compare JSONPath with jq and JMESPath on the same tasks, see jq vs JSONPath vs JMESPath.