DevKitHub

JSON & Data

JSONPath Tester — RFC 9535 Queries, Filters and Normalized Paths

Write a JSONPath query and see the nodes it selects from your JSON, each with its Normalized Path. Queries are evaluated as the RFC 9535 standard defines them, not as any one library does.

Try a query against the sample bookstore:
36 lines
2 matches

Normalized paths

  • $['store']['book'][0]['title']"Sayings of the Century"
  • $['store']['book'][2]['title']"Moby Dick"
4 lines

Queries follow RFC 9535, not the Gössner or Jayway dialects: filters are written [?@.price < 10], functions exist only inside filters, and a result is always a list of nodes, even when one node matches.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

JSONPath was in use for seventeen years before it had a specification. Stefan Gössner’s 2007 article defined it loosely and handed filter expressions to the host language — JavaScript’s eval(), in the original — so what ?(@.price < 10) meant depended on the library. Jayway’s Java library added operators and path functions of its own, and the same query came to mean different things in different tools. RFC 9535, published in February 2024, is the first standard. This tester implements it exactly and is checked against all 706 cases of the official JSONPath Compliance Test Suite, so a query that is valid here is valid in every conforming implementation, and one that is rejected here is not standard JSONPath, whatever your current library accepts.

The query is parsed and type-checked before the document is looked at, as the RFC requires: it is valid or an error regardless of the data, and a valid query never fails at run time — an index past the end or a missing name simply selects nothing. A filter [?…] runs over the children of each node it is given — the elements of an array, or the member values of an object — with @ bound to each child in turn. Comparisons never convert types, so 1 == '1' is false, and a missing value is equal only to another missing value. The five functions, length(), count(), match(), search() and value(), exist only inside filters, and the RFC’s typing rules decide where each may appear: length(@.tags) must be compared with something, while match(@.name, 'A.*') stands on its own as a test.

The result is always a list of nodes, even when one node matches, shown both as values and as Normalized Paths such as $['store']['book'][0]['title'] — the RFC’s single canonical spelling of a location, with names in single quotes and negative indexes resolved. Results from .. are in pre-order: each node before its own descendants, array elements in index order. Object members come in the order JavaScript holds them, which the RFC allows because objects are unordered, so a member named "10" is listed before one named "a". match() and search() take I-Regexp (RFC 9485), a portable subset of regular expressions: the pattern is validated, a dot matches anything except \n and \r, and a pattern that uses something outside the subset, such as \d or a lookahead, makes the function false rather than quietly running with JavaScript’s meaning.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Functions cannot be called on a path.

$..book.length()
Why:
Jayway JsonPath lets a function end a path, so $..book.length() returns the number of books there. RFC 9535 has no path functions: a query only ever selects nodes, and functions exist only inside filter expressions.
Fix:
Select the books with $..book[*] and read the match count, or filter by a length: $..book[?length(@.title) > 15].

Script expressions such as [(@.length-1)] are not part of RFC 9535.

$..book[(@.length-1)]
Why:
Gössner’s original article computed indexes with script expressions evaluated by the host language, and its example for the last book is $..book[(@.length-1)]. The standard removed script expressions along with every other use of eval().
Fix:
Use a negative index: $..book[-1] is the last book, and $..book[-2:] the last two.

A single = is not a comparison in JSONPath. Use == to compare.

$..book[?(@.price = 8.95)]
Why:
A single = is assignment in most languages. Libraries that pass filters to JavaScript’s eval(), as Gössner’s original did, run it as an assignment instead of rejecting it; RFC 9535 has only == and != for equality.
Fix:
Write == for equality: $..book[?@.price == 8.95]. The parentheses are optional in RFC 9535.

"-" cannot appear in a name written after a dot.

$.headers.content-type
Why:
Dot notation accepts only names made of letters, digits, underscores and non-ASCII characters, not starting with a digit. Some libraries accept more, so a hyphenated key that worked elsewhere is rejected here and by every conforming implementation.
Fix:
Use bracket notation for any other name: $.headers['content-type'].

A filter on an object returns nothing.

Why:
A filter tests the children of the node it is applied to. $.store.bicycle[?@.color == 'red'] tests the bicycle's member values — the string red and the number 399 — and neither has a color. Jayway tests the object itself in that position, which is why the query works there.
Fix:
Apply the filter to the parent: $.store[?@.color == 'red'] selects the bicycle.

@.price == '8.95' matches nothing.

Why:
RFC 9535 never converts between types: the number 8.95 and the string '8.95' are not equal, and < or > between a number and a string is always false. Gössner's original evaluated filters as JavaScript, where 8.95 == '8.95' is true.
Fix:
Compare with a literal of the same type as the data: @.price == 8.95. A comparison between a number and a numeric string is flagged.

Frequently asked questions

How is RFC 9535 JSONPath different from Jayway or Goessner JSONPath?
Filters are written [?@.price < 10], without the parentheses Goessner required, and are parsed by JSONPath itself instead of being handed to eval(). There are no script expressions, no =~ or in operators, no path functions such as .length(), and == never converts types. A filter on an object tests its member values, where Jayway tests the object. And a result is always a list, where Jayway returns a bare value for a path that can only match once.
How do I get the length of an array with JSONPath?
Not as a query result: an RFC 9535 query selects nodes and never returns a computed value. Select the elements with $.store.book[*] and read the match count, or use length() inside a filter to select by size, as in $..book[?length(@.title) > 15]. $.store.book.length is a member named "length", and Jayway’s .length() is not standard.
What is a Normalized Path?
The RFC's one canonical way to write a node's location: $ followed by bracketed segments, with names in single quotes and indexes counted from zero. $..book[-1] and $['store']['book'][3] find the same node, but only the second is its Normalized Path. Every match here is listed with one, so you can see exactly where each result came from.
Which regular expressions do match() and search() accept?
I-Regexp, from RFC 9485: characters, the dot, classes such as [a-z], Unicode categories such as \p{Lu}, groups, | and quantifiers. \d, \w, lookarounds, backreferences and flags are not part of it, and a pattern that uses them makes the function false. match() must match the whole string and search() may match anywhere; ^ and $ act as anchors, as in RFC 9485’s JavaScript mapping and the compliance suite.
Is my JSON sent anywhere?
No. The query is parsed and run in your browser and nothing is uploaded, which matters because the document being queried is usually a real API response.

Last updated