Skip to main content

JSONPath Evaluator

Run JSONPath queries against a JSON document and see the matched values. Supports child, index, slice, wildcard and recursive-descent operators.

Reviewed by · Last reviewed

How to use the JSONPath Evaluator

  1. Paste the JSON document into the left input area. The parser is strict RFC 8259, so trailing commas, single quotes, JS-style comments, or stray BOM bytes surface as parse errors before the path even runs.
  2. Type the path expression in the second field starting with $. Mix dot notation ($.store.book[0].title) and bracket notation ($['store']['book'][0]['title']) freely; use integer indexes, slice ranges, wildcards, and recursive descent as the structure demands.
  3. Press Evaluate to walk the tree. Matched values are emitted as a JSON array in document order, even when the path resolves to a single value.
  4. Copy the result panel into your test fixture, your fetch script, or another tab. The full input and output stay client-side; nothing is uploaded to ZeroUtil or anywhere else.

What the evaluator does under the hood

The path string is tokenized into a small ordered stream of opcodes: root (the leading $), child for dot or quoted-bracket access, index for integer subscripts (with negative indexing for end-relative offsets), slice for end-exclusive Python-style ranges ([start:end] and [start:end:step]), wildcard for *, and recursive for the .. double-dot operator. The walker carries a working set of nodes through each opcode, growing or shrinking the set as the operators apply. The whole pipeline is a single pure function in JsonPathEvaluator.logic.ts with unit-test coverage, no eval calls, and zero runtime dependencies.

Recursive descent ($..name) is the most expensive operator because it visits every node beneath the current frontier. The implementation does a depth-first pre-order traversal and emits matches in document order, which is what every other JSONPath library does and matches the de-facto Goessner reference behavior. Filter expressions like [?(@.price < 10)] and script expressions like [(@.length-1)] are deliberately not supported because they require a sandboxed expression evaluator and bring questions about predicate safety on hostile input.

A worked example, operator by operator

Take the canonical Goessner bookstore document and watch how each operator narrows or widens the working set:

{
  "store": {
    "book": [
      { "title": "Sayings of the Century", "price": 8.95 },
      { "title": "Moby Dick", "price": 8.99 },
      { "title": "The Lord of the Rings", "price": 22.99 }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  }
}
  • $.store.book[0].title walks root, then the store child, then index 0 of the book array, then its title. Result: ["Sayings of the Century"].
  • $.store.book[*].price uses a wildcard to fan out across every book, returning [8.95, 8.99, 22.99] in document order.
  • $.store.book[-1].title takes the last book by negative index: ["The Lord of the Rings"].
  • $.store.book[0:2].title slices the first two books (end-exclusive): ["Sayings of the Century", "Moby Dick"].
  • $..price descends recursively, collecting every price below root including the bicycle: [8.95, 8.99, 22.99, 19.95].

The last query is the one that catches people out. Recursive descent visits the whole subtree, not just the first match, which is why it is right for "find every id regardless of nesting" and wrong when you meant a single node.

When this tool earns its keep

  • Pulling a specific value out of a deeply nested webhook payload (Stripe events, Shopify orders, GitHub action contexts) without writing a throwaway JS one-liner.
  • Building snapshot tests where you want to assert "every id in this OpenAPI document is a string", drafted as $..id first.
  • Explaining to a teammate why their $.users[*].email query returns an empty array because the actual key is users at depth 2, not depth 1.
  • Extracting a list of file paths from a Cloudflare Pages build manifest, a Vercel deployment response, or a Kubernetes kubectl -o json dump.
  • Sketching the JSONPath you intend to put into a Datadog monitor, a Logz.io alert, or a Postman test before committing the configuration.
  • Validating that a long-tail JSON document actually contains the field you expect at the depth you assumed, without instrumenting code.

Common pitfalls and edge cases

  • Recursive descent is greedy. $..price on a document with both store.bicycle.price and store.book[*].price returns every match, not the first. If you want only the books, anchor with $.store.book[*].price.
  • Slices are end-exclusive. [0:2] returns elements 0 and 1, not 0 through 2 inclusive. This matches Python and the JSONPath reference but burns engineers used to inclusive ranges.
  • Negative indexes wrap silently. [-1] is the last element, [-100] on a 5-element array is no match (not an error). Plan for both behaviors when generating paths programmatically.
  • Dot notation cannot reach keys with dots in them. A key literally named "file.json" needs bracket notation: $['files']['file.json']. Dot syntax would parse it as a nested path.
  • Wildcards on objects iterate values, not keys. $.headers.* returns the values of every header, in insertion order. There is no key-introspection operator in the JSONPath grammar.
  • Filter expressions are not supported. If your path contains ?(...), switch to a richer engine like jsonpath-plus, JMESPath, or jq. The omission is intentional.

JSONPath origins and the IETF RFC 9535

JSONPath was sketched by Stefan Goessner in 2007 as a JSON analog to XPath. The reference implementation was a snippet of JavaScript that pages copied for years without a formal specification. In 2024 the IETF published RFC 9535, the first standards-track JSONPath document, which fixes corner cases (Unicode normalization, whitespace handling, slice semantics) and defines a portable test suite. The grammar implemented here is the common subset both Goessner and RFC 9535 agree on; richer engines such as Burke Library's jsonpath-plus, Java's Jayway, and Python's jsonpath-ng add filter and script expressions on top.

Alternatives and when they beat this tool

Command-line jq is the right choice for transformations rather than extractions; it has a richer query language, supports streaming, and runs on gigabytes of data without the browser memory pressure. JMESPath (the AWS CLI's --query flag) is a different but related grammar with stronger filter and projection support; if you live in aws, az, or gh CLIs, prefer it. Inside Node, jsonpath-plus on npm covers the filter expressions this evaluator omits. JSON Pointer (RFC 6901) is the right choice when you only need a single, fully-specified path with no wildcards. Use this evaluator when you want to draft and verify a path interactively in the browser, do not want to install or learn another query language, and do not want to paste sensitive payloads into a remote service like jsonpath.com.

Frequently Asked Questions

Which JSONPath syntax does this tool implement?

The common subset most engineers reach for: <code>$</code> root, <code>.child</code>, <code>['child']</code>, <code>[N]</code>, <code>[-N]</code>, <code>[*]</code>, <code>[start:end]</code> end-exclusive slice, and <code>..name</code> recursive descent. Filter expressions <code>[?(@.foo)]</code> and script expressions are deliberately omitted - a richer engine like jsonpath-plus is the right pick if you need them.

Why are filter expressions not supported?

Filter expressions add a JavaScript-like predicate engine and the attendant safety questions about untrusted input. The evaluator here is a pure walker over the document tree, which keeps the scope small and the attack surface zero. For predicate filtering, pipe the matched array through a separate JS expression on your side, or use jsonpath-plus.

Does the evaluator upload my JSON anywhere?

No. The whole evaluation runs inside a Preact component in your browser. There is no fetch call or analytics beacon for the document or the query. Open the Network tab in DevTools to confirm.

How does recursive descent ($..name) behave?

It collects every value whose key is exactly <code>name</code> anywhere below the current node, in document order. <code>$..price</code> on a bookstore document returns the price of every book and the bicycle, in the order they appear in the source.

How is index -1 handled on an empty array?

The result set is empty. Negative indices wrap from the end: <code>[-1]</code> is the last element, <code>[-2]</code> is the second-to-last. If the resulting offset falls outside the array, the evaluator emits no match rather than throwing.

What is the difference between dot and bracket notation?

They address the same nodes. <code>$.store.book</code> and <code>$['store']['book']</code> are equivalent. Bracket notation is mandatory when a key contains a dot, a space, or other punctuation, for example <code>$['user-agent']</code> or <code>$['file.json']</code>. Dot notation is terser for plain identifier keys, so most engineers mix the two in one path.

Does an empty result mean my path is wrong?

Not necessarily. An empty array is a valid, correct result when nothing matches - for instance <code>$.users[*].email</code> on a document with no users. The most common cause of an unexpected empty result is an off-by-one in the nesting depth or a key typo, so verify the document structure with <code>$.*</code> or <code>$..*</code> to see what is addressable.

How do slices with a step work?

<code>[start:end:step]</code> mirrors Python. <code>[0:6:2]</code> takes indices 0, 2, and 4 from the slice window; <code>end</code> stays exclusive. Omitting a component uses the default - <code>[::2]</code> is every other element from the start, <code>[1:]</code> is everything from index 1 onward.

Can I run the same query against many documents?

The evaluator works one document at a time. To batch over a directory, confirm the path here, then run it with <code>jq</code> or a Node script using <code>jsonpath-plus</code>. The tool is best for getting the path right, not high-volume extraction.

Does the tool show me which node each match came from?

The output is the array of matched values in document order. If you need the location of each match rather than its value, use a JSON Pointer engine or the <a href="/tools/json-diff-viewer/">JSON Diff Viewer</a>, which reports structural position.

More Developer Tools