Skip to the content
GigaJSON
All tools
Format and check
View and query
Convert
Encode and decode
Sample files
Guides Open GigaJSON

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 jsonpath runs)

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'', give syntax 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.title works without [*]. Add strict at the start and the same path fails with jsonpath member accessor can only be applied to an object.
  • $.**.price in lax mode returned all four prices twice, eight values. strict $.**.price returns 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 json to 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 reads app as the key.
  • [-1] works here and gives the last item.
  • There's no .length. Count with kubectl get pods --no-headers | wc -l instead.

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.

Questions

How do I filter by a value in JSONPath?

Put the condition in [?( )], with @ for the item being tested: $.store.books[?(@.price < 10)].title. That form works in jsonpath-plus, jsonpath-ng and python-jsonpath. PostgreSQL writes it as $.store.books[*] ? (@.price < 10).title, and kubectl as {.items[?(@.price<10)]}.

Why does my JSONPath filter return nothing?

Usually the library expects a different syntax and returns an empty result instead of an error. jsonpath-plus needs parentheses around the filter and doesn't accept [-1]. jsonpath-ng needs & instead of && between conditions. PostgreSQL wants double quotes around strings.

Does PostgreSQL support JSONPath?

Yes, since version 12, as SQL/JSON path: jsonb_path_query(), jsonb_path_exists(), and the @? and @@ operators. Its filters use ? ( ) after the list, strings in double quotes, and functions such as .size() and like_regex. A GIN index with jsonb_path_ops speeds up @? and @@ for equality checks.

How do I use JSONPath with kubectl?

Pass a template with -o jsonpath: kubectl get pods -o jsonpath='{.items[*].metadata.name}'. Wrap each path in { }, use {range .items[*]} ... {end} for one line per item, and escape dots in label keys: {.metadata.labels.app\.kubernetes\.io/name}.

More from the blog