How to extract fields from api responses using jsonpath
- Step 1Drop or paste one API response — Provide a single JSON document — one REST response, or the GraphQL
dataenvelope. Stacking multiple responses or raw NDJSON will failJSON.parse; wrap multiple records in a JSON array first. - Step 2Type the JSONPath expression — Enter the path in the expression box, e.g.
$.data.users[*].emailfor every email, or$.orders[?(@.status == 'pending')]for the first matching object. The placeholder examples ($.users,$.items[0],$.*.name,$..email) show the supported shapes. - Step 3Decide whether to wrap in an array — Leave the
Always wrap result in arraycheckbox off to get a bare value when exactly one node matches (handy for a single cursor or id). Turn it on when you want a consistent array even for one match — useful when feeding a fixture that always expects a list. - Step 4Click Extract and read the match count — The result panel shows the matched node(s) and a count like
3 matches.0 matchesmeans the path resolved to nothing — narrow it one segment at a time ($.data, then$.data.users) to find where it breaks. - Step 5Copy or download the result — Use Copy for the clipboard, or Download to save a
.extracted.jsonfile. Pretty-printing uses a two-space indent. - Step 6Port the path into your handler — Once verified, reuse the same path in Node with
jsonpath-plus, in Python withjsonpath-ng, or as object access now that you know the exact nesting. For full transforms beyond extraction, reach for jq or the sibling JSON tools.
Supported JSONPath syntax (verified against the evaluator)
Exactly what this tool's JSONPath subset resolves. Anything not listed here is unsupported and will return 0 matches rather than an error.
| Syntax | Meaning | Example | Result on a sample response |
|---|---|---|---|
$ | The whole document | $ | Returns the entire parsed response as one match |
.key | Child by name | $.data.user.email | The single email value |
["key"] / ['key'] | Child by quoted name (for keys with dashes/spaces) | $["user-id"] | Resolves a key dotted notation can't reach cleanly |
[n] | Array element by index (zero-based, no negatives) | $.items[0] | First element; [-1] returns 0 matches |
.* | All direct children (object values or array items) | $.data.* | Every value one level under data |
..key | Recursive descent — key at any depth | $..email | Every email key anywhere in the graph |
..* | Every node recursively | $..* | Flattens the whole document into a node list |
[start:end:step] | Array slice (Python-style, negatives + reverse) | $.items[0:5], $.items[::-1] | First five; full array reversed |
[?(@.k OP v)] | Filter array/object values by one predicate | $.orders[?(@.total > 100)] | Objects whose total exceeds 100 |
[?(@.k)] | Existence test (key present, truthy not required) | $.users[?(@.email)] | Every user that has an email key |
Filter operators and how values are compared
Filters take exactly one operator. There is no && / || — a compound expression silently returns 0 matches. Values are coerced by JavaScript, so type matters.
| Operator | Predicate form | Notes / gotcha |
|---|---|---|
== | [?(@.status == 'paid')] | Strict equality after the literal is parsed; 'paid', "paid", and paid are all read as the string paid |
!= | [?(@.role != 'admin')] | Strict inequality; missing keys (value undefined) are not equal to a string, so they pass |
> < >= <= | [?(@.amount >= 1000)] | Numeric compare via JS coercion — a field stored as the string "9.99" still compares numerically, which can surprise you |
| existence | [?(@.deletedAt)] | True whenever the key exists at all — even deletedAt: null matches, because the test checks presence, not truthiness |
| dotted path | [?(@.customer.tier == 'gold')] | The left side may walk nested keys, e.g. @.customer.tier, before applying the operator |
Cookbook
Real REST and GraphQL response shapes with the exact path that pulls each field. Tokens and emails are anonymised; [matches: N] is the live count the tool shows.
Pull every email from a paged user list
ExampleA typical list endpoint nests records under data.users. [*] fans out across the array, then .email projects one field. With wrap off you get a clean array because there is more than one match.
Path: $.data.users[*].email
Input:
{ "data": { "users": [
{ "id": 1, "email": "ada@x.com" },
{ "id": 2, "email": "grace@x.com" }
] } }
Output [matches: 2]:
[ "ada@x.com", "grace@x.com" ]Grab a single pagination cursor as a bare value
ExampleWhen you need just the cursor to fire the next request, leave wrap off so a single match returns the raw string instead of a one-element array.
Path: $.data.pageInfo.endCursor
Input:
{ "data": { "pageInfo": {
"hasNextPage": true, "endCursor": "Y3Vyc29yOjI1"
} } }
Output [matches: 1]:
"Y3Vyc29yOjI1"Isolate the one order that is still pending
ExampleA single-operator filter targets the object you care about inside an array. Returns the whole matching object so you can inspect every field.
Path: $.orders[?(@.status == 'pending')]
Input:
{ "orders": [
{ "id": "A1", "status": "paid" },
{ "id": "A2", "status": "pending" }
] }
Output [matches: 1]:
{ "id": "A2", "status": "pending" }Find a deeply nested id without knowing the depth
ExampleGraphQL responses bury ids under connection/edge/node wrappers. Recursive descent skips the structure and collects every id key, regardless of how deep it sits.
Path: $..id
Input:
{ "data": { "viewer": { "id": "u_1",
"repos": { "edges": [ { "node": { "id": "r_9" } } ] }
} } }
Output [matches: 2]:
[ "u_1", "r_9" ]Filter on a nested attribute, then project a field
ExampleThe filter walks a dotted path (@.customer.tier) before comparing, then .total projects one value out of the matched object.
Path: $.orders[?(@.customer.tier == 'gold')].total
Input:
{ "orders": [
{ "total": 50, "customer": { "tier": "gold" } },
{ "total": 10, "customer": { "tier": "free" } }
] }
Output [matches: 1]:
50Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Two responses pasted back to back
Parse errorThe tool runs JSON.parse on the entire input once. Two JSON objects with no array wrapper throw Unexpected non-whitespace character after JSON. Capture and test one response at a time, or wrap multiple in [ ... , ... ].
Negative array index like `[-1]`
0 matchesIndex access does not support negatives — $.items[-1] resolves to nothing. To grab the last element use a reverse slice and take the first: $.items[::-1][0], or a slice such as $.items[-1:].
Compound filter with `&&` or `||`
0 matchesFilters accept a single predicate. [?(@.age > 18 && @.active == true)] silently returns 0 matches because the parser only recognises one operator. Apply one condition here, then narrow the rest in your own code.
Existence test on a key set to null
Matched[?(@.deletedAt)] matches an object even when deletedAt: null, because the test checks that the key is present, not that it is truthy. To exclude nulls, compare explicitly with [?(@.deletedAt != null)].
Numeric filter on a string-typed field
By design[?(@.price > 5)] compares via JavaScript coercion, so a field stored as "9.99" still matches. This is convenient but means "10" < "9" style surprises can occur with mixed types — normalise types upstream if precision matters.
GraphQL errors array instead of data
0 matchesIf the server returned { "errors": [...] } with no data, a path like $.data.user yields 0 matches. Check $.errors[*].message to read the failure, then fix the query before extracting.
Path points at a key that doesn't exist
0 matches$.data.usres.email (typo) returns an empty result, not an error. Rebuild the path one segment at a time — $.data, then $.data.users — to see where the chain breaks.
Document larger than the free limit
BlockedFree tier caps input at 2 MB. A large list endpoint dump beyond that is blocked with an upgrade prompt; Pro extends the ceiling to 100 MB. For one-off field discovery, paste a single representative record instead of the full page.
Trailing comma or JS-style comment in the pasted JSON
Invalid JSONJSON.parse is strict — trailing commas, comments, and single-quoted strings throw. Run the payload through the JSON format fixer or validator first, then extract.
Frequently asked questions
Does the tool support the full JSONPath spec?
No — it implements a practical subset: $, .key, ["key"], [n], .*, ..key, ..*, slices [start:end:step], and single-predicate filters [?(@.k OP v)] plus existence [?(@.k)]. There is no script evaluation, no &&/||, and no functions like length(). Anything outside the subset returns 0 matches rather than erroring.
Can I paste a whole GraphQL response?
Yes. Paste the entire { "data": {...} } (and errors) envelope as one document and target fields with $.data.... It must be a single JSON object — not several responses concatenated.
Why does my `&&` filter return nothing?
Compound predicates are not supported. [?(@.a == 1 && @.b == 2)] silently yields 0 matches. Filter on the more selective condition here, then apply the rest in your handler code.
How do I get the last element of an array?
Negative index access ([-1]) is unsupported and returns nothing. Use a reverse slice: $.items[::-1] reverses the array, so $.items[::-1][0] gives the last item; $.items[-1:] also works because slices accept negatives.
What does the 'Always wrap result in array' checkbox do?
Off (default), a single match returns the bare value (e.g. "u_1"); multiple matches always return an array. On, every result is an array even when one node matches — useful when downstream code always expects a list.
Is the response data uploaded anywhere?
No. Parsing and JSONPath evaluation run entirely in your browser. API responses, tokens, and customer PII are never sent to JAD Apps servers.
Can I filter on a nested field inside the predicate?
Yes. The left side of a filter may walk dotted keys, e.g. [?(@.customer.tier == 'gold')], before the operator is applied.
What's the difference between `..key` and `.*`?
.* matches direct children one level down; ..key recursively finds every occurrence of that key at any depth. Use .* when you know the level and ..key when you don't.
Does existence `[?(@.x)]` mean the value is truthy?
No — it tests that the key is present. An object with x: null or x: false still matches. Add an explicit comparison ([?(@.x != null)]) if you need a non-null value.
How big a response can I process?
Free tier handles documents up to 2 MB; Pro raises it to 100 MB. For field discovery you rarely need the full page — a single record is enough to confirm the path.
I need to extract from many responses at once — can this do it?
Not directly; it parses one document per run. Wrap the records in a single JSON array first, or if you have CSV/spreadsheet data convert it with csv-to-json and then extract.
Can it transform or rename fields, not just extract?
No — it only reads nodes out. To rename keys use json-key-renamer, to drop keys use json-key-filter, and for arbitrary reshaping reach for jq.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.