JSON Path Tester

Write an expression, watch every matching node light up in the tree beside it, then take away the values or the exact paths the engine walked to reach them. Click any key to have its path written for you.

Ready

Document

Result

JSONPath started as a blog post. Stefan Goessner published it in February 2007 as a way to do for JSON what XPath does for XML, sketched the grammar in a page of prose, and shipped a 300 line JavaScript reference implementation. Ports appeared in Java, Python, Go, PHP, C# and half a dozen other languages, each one filling the gaps in the sketch differently. Seventeen years later, in February 2024, RFC 9535 turned the sketch into a real specification. Most of the tooling you will meet today still runs the older dialect, which is why the same expression sometimes returns different answers in two systems.

What the three panels do

The bar at the top holds one expression. Every keystroke re-runs the query against the parsed document after a short pause, so the match count beside the Run button moves as you type. A green count means the engine found nodes. An amber count means the expression parsed cleanly and matched nothing, which is a different situation from a broken query and gets reported as such.

The left pane holds the document in two views. The tree is the interesting one. Every matched node gets an amber row, ancestors of a match open themselves so you never hunt through a collapsed branch for a hit, and clicking any key writes that node's exact path into the expression bar. Building a query by clicking down through real data beats guessing at bracket syntax, particularly when keys carry dots or spaces in them. Raw view is a plain editable textarea. Paste into either one.

The right pane has three formats, and the second is the one most testers leave out:

Everything runs in the browser. The document never leaves the page, so pasting a production response with real customer records carries no upload risk.

The syntax, one operator at a time

An expression is a chain of steps applied left to right. Each step takes the set of nodes produced by the step before it and returns a new set. Start with the root, narrow as you go.

OperatorMeansAgainst the sample document
$The root node, where every expression startsThe whole payload as a single match
.keyOne named child$.region returns eu-west-1
['key']The same child in bracket form, needed when a key has a dot, a space or a dash in it$['build']['commit']
*Every child of the current node$.limits.* returns the three numbers
..keyRecursive descent, matching the key at any depth$..owner reaches into every endpoint
[n]One array element by index, counting from zero$.hosts[0]
[a,b]A union of two or more indexes or namesIn the grammar since 2007, rejected outright by the engine behind this page. See the disagreements below
[start:end]A slice, with end excluded, the same rule Python uses$.endpoints[1:3] gives elements 1 and 2
[-1:]A slice counting back from the end$.hosts[-1:] gives the last host
[?(@.x > 1)]A filter, keeping array elements where the test holds$.endpoints[?(@.p95Ms > 100)]

The two dot operator is the one people underuse. $..price finds every price in a document without you knowing the shape, which is exactly the job when a vendor sends back four levels of wrapping around the field you wanted. The cost is speed on large documents, since recursive descent visits every node, and ambiguity when the same key name means two different things in two branches.

Filters, and the symbol that trips people up

Inside [?( )] the at sign stands for the element being tested, the way $ stands for the root. So $.endpoints[?(@.p95Ms > 100)] reads as: walk the endpoints array, and for each element, keep it when its p95Ms field is over 100. The filter returns whole elements, not fields. Add a step after the bracket to pull a field out of the survivors.

Comparisons behave as you would expect for numbers and strings. Equality is a double equals in most implementations, though a few accept a single one. A bare [?(@.isbn)] with no comparison tests existence, which is the shortest way to split records that carry an optional field from records that do not.

FilterWhat comes back
[?(@.deprecated == true)]The legacy endpoint, the only one with the flag set
[?(@.owner)]Endpoints with an owner, skipping the one where owner is null
[?(@.method == 'POST')]String comparison, single quotes inside the filter
[?(@.errorRate > 0.01 && @.p95Ms > 200)]Two conditions joined, supported by most engines but not all
[?(@.tags.length > 1)]A property lookup inside the filter, and a common portability trap

Null is where filters catch people out. In the sample document one endpoint has "owner": null. A test for existence treats that as absent in most engines, since null is falsy, so $.endpoints[?(@.owner)] returns three endpoints rather than four. If you meant "the key is present", you need [?(@.owner != null)] or an engine that separates presence from truthiness. RFC 9535 draws that line clearly. The older implementations mostly do not.

Where implementations disagree

This matters more than the syntax table, because an expression verified in one tool is not portable by default. The gaps below are the ones that cost real debugging time.

The practical rule: verify an expression against the engine you will actually deploy on, not against a generic tester. This page runs the dchester JavaScript implementation, which is the one behind a great many Node tools and Postman scripts. An expression working here has a good chance of working in jsonpath-ng or Jayway, and no guarantee.

Where you meet JSONPath in real systems

The language shows up in more places than its low profile suggests, usually as the glue between two systems nobody wants to write code between.

SystemHow it uses JSONPath
kubectl-o jsonpath pulls fields out of resource objects. Its dialect is its own: expressions sit inside curly braces, it adds a range construct, and filter support is thinner than the reference grammar
AWS Step FunctionsInputPath, ResultPath and OutputPath route data between states with JSONPath. Getting a path wrong here fails the execution rather than returning empty
Postman and NewmanTest assertions read response fields by path, and the runner ships the same JavaScript engine this page uses
Grafana and SplunkJSON data sources map a path to a series or a field before anything gets plotted
Azure Logic AppsTrigger conditions and data operations accept JSONPath expressions against webhook bodies
Karate and REST AssuredAPI test frameworks assert on response bodies by path, Jayway underneath in the Java case

Because each of those carries its own dialect, use the Paths tab here as a portability check. If a query returns the paths you expected, those literal paths are usually accepted by every engine, even where the clever expression that produced them is not.

JSONPath, JSON Pointer and jq are three different things

They get confused constantly, and picking the wrong one wastes an afternoon.

JSON Pointer is RFC 6901, a much older and much smaller standard. A pointer looks like /endpoints/0/path and addresses exactly one location. No wildcards, no filters, no recursion. That limitation is the point: a pointer is unambiguous, which is why JSON Patch, JSON Schema errors and OpenAPI references all use pointers rather than paths. Reach for a pointer when you need to name one node. Reach for JSONPath when you need to search.

jq is a full stream processing language with its own syntax, and reads similarly at first glance. .endpoints[].path in jq resembles $.endpoints[*].path in JSONPath, then jq keeps going into map, reduce, string interpolation and user defined functions. JSONPath selects. jq selects and transforms. If your expression is growing a pipe, you have outgrown JSONPath.

JMESPath sits between the two and powers the AWS CLI's --query flag. Its syntax is close enough to JSONPath to mislead and different enough to break, most visibly in filters, which JMESPath writes as [?p95Ms > `100`] with backticks and no at sign.

Writing expressions that survive contact with real data

$.endpoints[?(@.errorRate > 0.01)].path // the paths that need attention$..endpoints[*].owner // every owner, wherever endpoints appear$['limits']['requestsPerMinute'] // one node, unambiguous, safe to store

Where this page stops

The document has to be a single valid JSON value. NDJSON, JSON with comments and trailing commas are rejected by the parser, so run those through the JSON fixer first. The tree stops drawing at 2500 rows to keep scrolling smooth, though the expression still runs against the whole document and the result panel stays complete. Documents past a few megabytes will feel slow, since parsing and rendering both happen on the main thread. Script expressions in the [( )] form still evaluate here, because the engine carries its own evaluator, and RFC 9535 drops them, so an expression leaning on one will not travel. Index unions such as [0,3] go the other way and fail here despite being legal JSONPath. Results follow the dchester JavaScript engine throughout, so the disagreements listed above apply: treat this as a fast way to work out what you meant, then confirm against the engine you ship on.

JSONPath questions that come up in practice

Filter behaviour, portability between engines, and the differences people hit on their first real payload.

What does the $ mean at the start of a JSONPath expression?

It names the root of the document, the node every expression starts from. Steps after it walk downward, so $.endpoints[0].path reads as: from the root, take the endpoints array, take its first element, take that element's path field. Inside a filter the at sign plays the same role for the element currently being tested.

What is the difference between $.store.book and $..book?

The first walks one exact route: a store key at the root, then a book key inside it. The second searches every level of the document for any key named book and returns all of them. Recursive descent is useful when you do not know the shape of a payload, and risky in production code, because a new field with the same name anywhere in the response changes the result.

Why does my filter expression return nothing?

Four causes cover most cases. The filter is applied to an object rather than an array, which several engines reject. The compared value is a string in the data and a number in your expression. A single equals was used where the engine wants two. Or the field is null, which most engines treat as absent for an existence test. The Paths tab shows what did match, which usually points at the mismatch quickly.

Does a filter return the matching field or the whole record?

The whole element. $.endpoints[?(@.p95Ms > 100)] gives complete endpoint objects, not the p95Ms numbers. Add a step after the closing bracket to pull one field out: $.endpoints[?(@.p95Ms > 100)].path. This catches almost everyone once.

How do I select a key that contains a dot or a space?

Use bracket notation with quotes: $['user.name'] or $['first name']. Dot notation splits on the dot, so $.user.name would look for a name key inside a user object instead of the single key you meant. Bracket form also handles keys starting with a digit and keys with dashes.

Is JSONPath the same as JSON Pointer?

No. JSON Pointer is RFC 6901 and addresses exactly one location with a slash separated string such as /endpoints/0/path. It has no wildcards, filters or recursion, which is deliberate: JSON Patch and JSON Schema need an unambiguous address. JSONPath is a query language built for searching a document whose shape you may not know.

Why does the same expression give different results in another tool?

Because JSONPath had no formal specification between 2007 and 2024. RFC 9535 was published in February 2024, and libraries are still catching up. Ordering after recursive descent, negative slice steps, the length property and filters over objects are the four places implementations most often differ. Verify against the engine you deploy on.

How do I get the last element of an array?

Use the slice $.hosts[-1:], which is the portable spelling. The bare $.hosts[-1] returns the last element in several engines and returns nothing in the one running this page, so it is the wrong habit to build. The old script form $.hosts[(@.length-1)] appears in tutorials, relies on evaluating code at query time, and has been dropped from most maintained libraries and from RFC 9535.

Can I use JSONPath to change a value, not only read one?

Some libraries add it. The dchester JavaScript package has an apply method, and jsonpath-ng in Python has update. The query language itself only selects. When you need to write, take the normalized path from the Paths tab here and hand that to whatever mutation API your library offers, since a concrete path removes the ambiguity a wildcard would introduce.

Does kubectl use standard JSONPath?

It uses a dialect. Expressions sit inside curly braces, there is a range construct for iterating that no other engine has, and filter support is thinner than the reference grammar. Expressions verified here often need reshaping for kubectl. Simple field paths carry over unchanged, which is why the Paths tab is a good starting point.

Is my JSON sent to a server?

No. Parsing, tree rendering and query evaluation all happen in your browser through JavaScript on this page. Nothing is uploaded, logged or stored, so pasting a production response with real records is safe. Closing the tab discards everything.

What is the largest document this handles?

Files up to a few megabytes are comfortable. Past that, parsing and tree rendering compete for the main thread and typing starts to lag. The tree caps at 2500 drawn rows, though the query still runs over the whole document, so the result panel stays accurate on documents larger than the tree shows.

Copied