JSONPath tester

Test RFC 9535 JSONPath queries against JSON: selectors, filters, all five functions, and the normalized path of every match — all in your browser.

JSONPath query
JSON
Matches: 2
  • Path$['store']['book'][0]['title']
    "Sayings of the Century"
  • Path$['store']['book'][2]['title']
    "Moby Dick"

A query language that finally has a standard

JSONPath is to JSON what XPath is to XML: a small language for pointing at parts of a document. You write an expression like $.store.book[0].title and it selects the nodes that match. The idea comes from a 2007 blog post by Stefan Goessner, and for seventeen years that post was the only reference — which meant every library implemented the gaps differently. What does a bare $.. select? Is $[1,2] guaranteed to come back in order? How does a filter compare a value that is not there? Ask three JSONPath libraries and you could get three answers.

RFC 9535, published in 2024, finally pinned all of it down. This tester targets that standard, not the folklore. It runs the query the way the RFC says, shows you each match together with its exact location, and — importantly — refuses a query that is valid in some older dialect but not in RFC 9535, telling you where the problem is rather than quietly doing something non-standard that another tool would do differently.

Segments and selectors

A query is the root identifier $ followed by a sequence of segments, and each segment applies one or more selectors to the nodes it receives. There are five selectors:

  • Name — $.store or $["store"] selects the value of a member. The dot form is shorthand; the bracket-and-quote form works for any key, including one with spaces or punctuation.
  • Wildcard — * selects every member of an object or every element of an array.
  • Index — [0] selects an array element, and a negative index counts from the end, so [-1] is the last.
  • Slice — [start:end:step] selects a range, exactly like Python: [1:3] is elements 1 and 2, [::-1] reverses, [::2] takes every other one.
  • Filter — [?<expression>] keeps only the elements or members for which the expression is true (covered below).

A bracket can hold several selectors at once: [0, 2, "title"] selects three things in one segment. And a segment can be a child segment (a single dot or bracket) or a descendant segment (.. ), which searches the node and all of its descendants — $..author finds every author anywhere in the document.

Filters, and the four ways to compare nothing

A filter selector tests each element with a logical expression, in which @ refers to the current element and $ to the whole document. The expression can compare values (==, !=, <, <=, >, >=), combine tests with && and || and !, and simply test for existence: $..book[[email protected]] keeps the books that have an ISBN, because @.isbn selects a node only when the key is present.

The subtle part is comparing something that is not there. A query on the left or right of a comparison yields either a value or nothing. The RFC defines this precisely: nothing equals nothing, nothing equals no actual value, and any ordering test (<, >) against nothing is simply false. So @.price < 10 quietly skips an element that has no price rather than erroring — which is usually what you want, and always what the standard says.

The five functions

RFC 9535 adds five functions you can call inside a filter:

  • length() — the length of a string (counted in characters), array or object.
  • count() — how many nodes a query selects, so you can filter on cardinality: [?count(@.chapters) > 3].
  • value() — the single value a query selects, or nothing if it selects zero or many.
  • match() — whether a string matches a regular expression in full.
  • search() — whether a regular expression is found anywhere in a string.

The standard is strict about how these are used, and this tester enforces it at parse time: length() takes exactly one value, so length(@.*) — a query that could select many nodes — is a syntax error, not a query that silently misbehaves. Likewise match() returns a true/false, so writing match(@.a, "x") == true is rejected, because a logical result is not something you compare.

The regular expressions are I-Regexp

match() and search() do not use JavaScript regular expressions; they use I-Regexp (RFC 9485), a small, portable subset designed to behave the same across languages. Most of it is what you would expect — character classes, quantifiers, alternation, Unicode properties like \p{Lu} for an uppercase letter. The one trap is the dot: in I-Regexp, . matches any character except a carriage return or line feed, which means it does match the Unicode line separators U+2028 and U+2029 that a JavaScript dot excludes. This tester compiles I-Regexp faithfully, so a pattern behaves here the way a conformant server would evaluate it.

The difference between the two functions is only anchoring: match requires the whole string to match, while search looks for the pattern anywhere within it. match(@, "a.*") accepts "abc"; search(@, "b") accepts any string containing a b.

Normalized paths, and running in the browser

For every match, this tester shows a Normalized Path — the canonical location the RFC defines, written with the bracket-and-quote form: $['store']['book'][0]['author']. Unlike the query, which may select many nodes, a normalized path points at exactly one, using only name and index selectors and a fixed quoting style. It is the answer to "where did this match come from", and it is what lets you turn a wildcard result back into a set of concrete locations. Most testers show the values and leave you to work out the paths; this one shows both.

The whole engine runs in your browser — the JSON is parsed and the query evaluated on your own device, and nothing you paste is uploaded, stored or logged. It is validated against the official JSONPath Compliance Test Suite, so its answers agree with the standard rather than with one library's interpretation of it.

Frequently asked questions

Which JSONPath dialect does this use?
RFC 9535, the 2024 IETF standard, and it is validated against the official JSONPath Compliance Test Suite. It deliberately does not accept the older Goessner conventions where they diverge from the RFC; such a query is rejected with the position of the problem so you can fix it.
Why was my query rejected when it works in another tool?
Because that tool follows the pre-standard Goessner conventions, which differ from RFC 9535 in several places — a bare $.., script expressions like [(@.length-1)], leading-zero indices, and unquoted names with special characters are all non-standard. The RFC replaced these with well-defined equivalents, and this tester holds to the RFC.
What is a normalized path?
The canonical location of a single node, written in the bracket-and-quote form the RFC defines, e.g. $['store']['book'][0]['author']. A query can match many nodes; each match has exactly one normalized path, which is why the tester shows it beside every value.
How does a filter treat a missing value?
As nothing, with rules the RFC pins down: nothing equals nothing, nothing is not equal to any real value, and any ordering comparison (<, >) involving nothing is false. So @.price < 10 simply skips an element with no price rather than raising an error.
Do match() and search() use JavaScript regular expressions?
No. They use I-Regexp (RFC 9485), a portable subset. The main practical difference is the dot, which matches everything except carriage return and line feed — including U+2028 and U+2029, which a JavaScript dot excludes. This tester compiles I-Regexp faithfully so results match a conformant implementation.
What is the difference between match and search?
Anchoring. match() requires the entire string to match the pattern, as if it were wrapped in anchors; search() succeeds if the pattern is found anywhere in the string. Everything else about the two is identical.
Is my JSON sent to a server?
No. The document is parsed and the query is evaluated entirely in your browser, and nothing you paste is uploaded or logged. It is safe to test against a real API response or a configuration file.