JSONPath Tester

Paste a document, type an expression, see what it selects. Filters work, so [?(@.price < 10)] returns rows rather than an error. Flip the output to Paths and you get the exact location of every match instead of its value. Your JSON stays in this tab and your expression is never run as code.

Want the matches as a table instead? Open the app

Nobody writes a JSONPath on the first try

You write one, it returns an empty array, and now there are two questions: is the expression wrong, or is the data not what you thought? A tester exists to separate those two. The situations are always some flavour of:

  • An API response you did not design. Four levels of envelope around the two fields you actually want, and the documentation stops one level short.
  • An expression that has to go into a config file. Kubernetes kubectl -o jsonpath, a CI pipeline, a Postman test, an integration platform's mapping step. Getting it right in a box that shows you the answer beats getting it wrong in a pipeline that shows you a stack trace.
  • A filter you are not sure of. "Every line item under fifty" is easy to say and easy to write three subtly different ways, only one of which is right.
  • A field that is missing from some records. Half the objects have a meta block and half do not, and you need to know whether your filter copes or explodes.
  • Finding out where something lives. You know a value is in there somewhere. What you need is its address, so you can write a stable expression for it.
  • Learning the syntax. The difference between $.store.book[*] and $..book[*] lands much faster when you can see both answers side by side.

The awkward part is that the tester most people land on has been sitting still for years. Filters that fail, no path output, and a box you are asked to paste production data into. All three are worth fixing.

Worked example: the cheap books

"Try an example" loads Goessner's store, the document every JSONPath article uses, as store.json:

{
  "store": {
    "book": [
      { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 },
      { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 },
      { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  }
}

Now the query that a broken tester cannot answer. Put $..book[?(@.price < 10)].title in the expression box, leave Show on Values, and the panel reads "2 matches" over this:

[
  "Sayings of the Century",
  "Moby Dick"
]

Two books under ten, by title, with the twelve-ninety-nine one left out and the red bicycle at 19.95 never considered because it is not a book. The download button hands you that array as store-matches.json.

Switch Show to Paths and run the identical expression. Same two matches, different answer:

[
  "$['store']['book'][0]['title']",
  "$['store']['book'][2]['title']"
]

Those are addresses, not values. Index 0 and index 2, so you now know which array positions the filter picked without counting objects by eye. Each one is a complete expression in its own right: paste $['store']['book'][2]['title'] back into the box and it returns ["Moby Dick"]. That round trip is the whole point of the bracket-notation form. It never depends on a wildcard matching the same things next time.

A few more against the same document, all of which return what you would hope: $..book[?(@.isbn)].title gives ["Moby Dick"], the only one with an ISBN. $..book[?(!@.isbn)].title gives the other two. $..book[?(@.category == "fiction" && @.price < 10)].title narrows to ["Moby Dick"] alone. And $..book[?(@.publisher.country == 'US')].title, over books that have no publisher at all, returns an empty array rather than throwing.

How the filters run without running your code

Filters are the reason a JSONPath tester is worth opening, and they are also the reason many of them are risky. The classic implementation hands the inside of [?(...)] to eval, which means the expression box is a JavaScript console wearing a hat. Here it works differently:

  • Filters are parsed, not evaluated. The contents of a filter bracket are read into a syntax tree and that tree is walked by an interpreter. Nothing reaches eval, Function or a script sandbox, which is also why the page runs under a strict content security policy.
  • Script brackets are refused outright. The [(...)] form computes a property name from an arbitrary expression. It is checked for and rejected before the query engine sees the string, with a message that names the offending bracket and suggests the safe alternative.
  • Missing members do not throw. A filter that reaches through a member some objects do not have yields no match on those objects and carries on. This is what the specification asks for, and without it a single record missing one nested field would fail the entire query.
  • The document never leaves the tab. No upload, no round trip, no server-side query log with somebody's order history in it.
  • Nothing is read from the URL. The expression comes from the box you type in and nowhere else, so no link can arrive with a query pre-loaded.

What the expression box understands

Goessner-style JSONPath with the jsonpath-plus extensions, which is the dialect most libraries and most tutorials describe. In practice:

  • Roots and children. $.store.bicycle.color and its bracket twin $['store']['book'][0]['title'], which is the form to reach for when a key has a space or a dot in it.
  • Descendants and wildcards. $..price collects all four prices in the sample, books and bicycle together. $.store.book[*].author walks one level.
  • Slices and unions. $..book[1:].title takes everything from the second book on, $.store.book[0,1].title takes a specific pair, and $.store.book[-1:].title takes the last one.
  • Filters. Comparisons with <, <=, >, == and !=, existence tests such as [?(@.isbn)], negation with !, and combinations joined by && or ||. String literals take either quote style.
  • Property names instead of values. A trailing ~ returns keys: $.store.*~ gives ["book", "bicycle"].
  • Parents. A trailing ^ steps up from each match to the thing containing it, which saves a lot of rewriting when you have found the right leaf but want the whole object.
  • Both output modes. Values gives you what matched, Paths gives you where. Switching between them re-runs the same expression on the document already in the box.

Gotchas worth knowing

  • Zero matches is a result, not a failure. An expression that selects nothing returns [] and a summary of "0 matches". That is the correct answer to a question about data that is not there, and it is the answer you should expect when a filter tests a field the document does not have.
  • Nonsense also returns zero matches. Type something that is not a path at all and you will usually get an empty array rather than a complaint. If a query surprises you with nothing, check the spelling of the expression before you conclude anything about the data.
  • A malformed filter does complain. When the contents of [?(...)] cannot be parsed you get a message pointing at the character it gave up on, with a reminder to check the brackets, the quotes and the comparison.
  • Script brackets are blocked, not broken. $.store.book[(@.length-1)] is a real JSONPath construct and it is deliberately refused here. Use $.store.book[-1:] for the last element, which is safer and reads better anyway.
  • The regex operator is not part of this dialect. A comparison written with =~ quietly matches nothing. Use a string method inside the filter instead: $..book[?(@.author.match(/^Nigel/))].title returns the Nigel Rees title.
  • Paths mode does not follow a moving target. A normalized path pins an array index. If the array is reordered on the next API call, that path points somewhere else. Use it to understand a document, not as a long-lived selector into a changing one.
  • The input must be a single JSON document. Newline-delimited JSON is several documents in one file, so parse it or convert it first. Anything that is not valid JSON stops with the parser's own message and the position it failed at.
  • Duplicate values look duplicated. Matches are not deduplicated, so two identical strings sitting in two different places produce two identical entries in Values mode. Paths mode is how you tell them apart.

Frequently Asked Questions

Do filter expressions actually work here?

Yes. Comparisons, existence tests, negation and combinations with && and || are all evaluated, so $..book[?(@.price < 10)].title returns two titles from the built-in example rather than an error or an empty array. Filters are the part of JSONPath people come to a tester to check, and a tester that quietly drops them is not worth using.

Is my JSON sent to a server?

No. The document is parsed in your tab and the expression is run there. There is no upload endpoint behind this page, no account and no history, which is the difference that matters when the thing you are debugging is a production API response with real customer records in it.

What is Paths mode for?

It answers where instead of what. The same query returns the location of every match in bracket notation, so $..book[?(@.price < 10)].title comes back as $['store']['book'][0]['title'] and $['store']['book'][2]['title']. Each one is unambiguous, each one identifies a single node, and each one can be pasted straight back into the box as a query of its own.

Why does my filter return nothing instead of an error?

Because a filter over a member that does not exist simply does not match. Query $..book[?(@.publisher.country == 'US')].title against a document whose books have no publisher and you get zero matches, not a thrown TypeError. That is the behaviour the JSONPath specification describes, and it is why filters over ragged real-world documents are usable here at all.

Why are script expressions refused?

A script bracket, written [(...)], computes a property name by running an arbitrary expression against your data. It is a very wide door for a feature almost nobody uses, so it is blocked before anything reaches the query engine and you get a message saying so. Everything a script bracket is normally used for has a safe equivalent: a plain index, a wildcard, a slice or a filter.

Which JSONPath dialect is this?

Goessner-style JSONPath with the jsonpath-plus extensions, which is what most libraries and most tutorials describe. That includes the extras Goessner's original did not have, such as ~ for property names and ^ for the parent of a match. Paths mode emits the bracket-notation normalized form, so what comes out of Paths mode goes back in as a query.

Try an expression

Free, no account, nothing uploaded, nothing evaluated as code. Paste your JSON, type a path, and switch between the values and their addresses.

Back to the tester