// blog/developer/
Back to Blog
Developer · Published June 16, 2026 · 7 min read · By Toine ·

Update note: Rewritten from experience; RFC 9535 filter syntax and the jsonpath-plus security fixes added, jq streaming claim corrected, JSON Path Finder and Tree Viewer linked

JSONPath: Query Nested JSON Without Counting Braces

JSONPath: Query Nested JSON Without Counting Braces

An API response comes back at 2,000 lines. The field you need is in there, four levels down, inside an array of objects that each hold another array. The documentation says so. Finding the exact path by eye is not reading, it is counting braces.

I do this most weeks, usually with a response from a supplier's system or a JSON blob copied out of a log. This post covers the JSONPath syntax that names a location so you can stop counting, the order in which I look when I do not know where a field is, and the jq one-liners that replaced most of the small scripts I used to write for this.

* * *

JSONPath in six symbols

JSONPath does for JSON what XPath does for XML: it describes where a value lives, so you can ask for it instead of walking to it. It was a convention for fifteen years, with small differences between libraries. Since 2024 it is an IETF standard, RFC 9535, and the newer libraries follow it.

The whole language fits in six symbols:

  • $ is the root. Every path starts there.
  • .name steps into a property. $.store is the store object at the root.
  • [0] picks an array element. [-1] is the last one.
  • matches everything at that level. $.store.book[].author is every author.
  • .. searches all descendants. $..price finds every price at any depth.
  • [?expr] filters. $.store.book[?@.price < 10] is the books under ten.

Older libraries write the filter as [?(@.price < 10)], with parentheses. RFC 9535 drops them, and most implementations accept both forms.

Libraries: jsonpath-plus for JavaScript, jsonpath-ng for Python, Jayway JsonPath for Java. One warning about the JavaScript one. Versions of jsonpath-plus before 10.0.7 evaluated filter expressions in a way that allowed code execution, and it took two CVEs, in 2024 and 2025, to close the hole. Update it, and never let user input into a filter expression in any library.

If you want the path to a value without installing anything, the JSON Path Finder shows the document as a tree in the browser. Click the value, and the path is there to copy.

Code editor showing a deeply nested JSON structure
Code editor showing a deeply nested JSON structure
* * *

How I find a field when I do not know the path

The syntax is the easy part. Sitting in front of a 50 KB response, knowing the value is in there somewhere, is the hard part. This is what I do, in this order.

Format it. Minified JSON on one line is not readable by anyone. The JSON Formatter indents it and, more usefully, says on the spot when it does not parse, with the line and character. Half the time the field I could not find was on the far side of a syntax error.

Search for a value you know. An ID, a name, a status string. Find the value, then walk up the indentation to build the path. This is faster than reading down from the top, every time.

Collapse to the skeleton. The JSON Tree Viewer shows the document folded, so you see the top-level keys first and open one branch at a time. Two or three levels in, the shape of the thing is usually obvious.

Ask the console. In the browser or a Node REPL:

`javascript const data = JSON.parse(rawJson); Object.keys(data); // top-level keys Object.keys(data.results[0]); // keys of the first result `

Once I have the path, that is what goes in the defect report, not a description of where to look. A path is a fact. "The status field in the second block" is an argument waiting to happen.

Key takeaway

The syntax is the easy part.

* * *

Flatten it when a table is what you need

Sometimes the right answer to nested JSON is to stop it being nested. A flat table is easier to scan, filter and sort, and it is what the people you send it to can open.

The JSON to CSV converter flattens a document into rows. The CSV to JSON converter builds nesting back up when a spreadsheet has to become an API request. There are three ways to flatten, and you should pick one on purpose.

Dot-notation keys. {"user": {"name": {"first": "Jane"}}} becomes a column called user.name.first. The hierarchy survives in the header. This is the default I want for anything going to Excel.

Unwinding arrays. A parent with three children becomes three rows with the parent fields repeated. MongoDB calls this $unwind. It is what a relational database wants, and it multiplies your row count, so count before and after.

Selective extraction. Do not flatten the document at all. Pull the five fields you need with JSONPath and leave the rest. This is the one that scales, and it is what $.. is for.

Arrays of uneven length are where flattening goes wrong. CSV rows want the same number of columns, and a record with two phone numbers next to one with five forces you to pad or to unwind. Decide before you convert, not after the columns have shifted.

Developer debugging API response data on screen
Developer debugging API response data on screen
* * *

jq replaces most of the scripts

For anything in a terminal, jq is the tool. It filters and restructures JSON from a file or straight out of curl, and its syntax is close enough to JSONPath that one carries over to the other.

`bash # Pretty-print jq '.' data.json

# One field jq '.results[0].name' data.json

# Every name in an array jq '.results[].name' data.json

# Filter jq '[.results[] | select(.status == "active")]' data.json

# Restructure jq '.results[] | {id: .id, fullName: (.first + " " + .last)}' data.json

# Count jq '.results | length' data.json `

Straight from an API:

`bash curl -s https://api.example.com/users | jq -r '.data[] | select(.role == "admin") | .email' `

Fetch, filter to admins, print the emails one per line thanks to -r. No temp file, no script.

One correction to a claim you will read elsewhere: jq does not stream by default. It reads each input document into memory before it does anything. For a file that does not fit, use jq --stream, or a streaming parser in your language.

In Python the standard json module plus a comprehension does the same job:

`python import json

with open('data.json') as f: data = json.load(f)

emails = [u['email'] for u in data['users'] if u['status'] == 'active'] `

Key takeaway

For anything in a terminal, `jq` is the tool.

* * *

Six JSON mistakes that cost the most time

These come up so often that knowing them in advance is worth more than any tool.

Trailing commas. {"a": 1, "b": 2,} is valid JavaScript and invalid JSON. Anyone who writes JSON by hand after a day in JavaScript does this.

Single quotes. Strings take double quotes, no exceptions. {'name': 'Jane'} will not parse.

Comments. JSON has none. Not //, not / /, not #. If a config file needs comments, use JSONC (VS Code settings and tsconfig.json use it) or YAML, and do not call the file .json.

Big integers. JavaScript's Number loses precision above 2^53. An ID of 9007199254740993 comes out of JSON.parse as 9007199254740992, and nothing warns you. It is a nasty one to find because the number still looks plausible. Send large IDs as strings, or parse with BigInt.

Encoding. The standard says UTF-8 when JSON moves between systems. A Latin-1 export with an é in a surname gives you either a parse error or a garbled character, depending on which is worse for your day. Keep the whole pipeline UTF-8.

Nulls on the path. data.user.address.city throws when address is null. Use optional chaining, data?.user?.address?.city, or the null-safe equivalent in your language, and decide up front what a missing city should mean.

The JSON Formatter catches the first three the moment you paste. For the shape of the data rather than the syntax, write a JSON Schema and run samples through the JSON Schema Validator.

* * *

FAQ

Is there a maximum nesting depth in JSON?

The standard sets none. Parsers do: JSON.parse in JavaScript copes with hundreds of levels, streaming parsers with more. If your own data is more than five or six levels deep, the problem is the design, not the parser.

Does JavaScript support JSONPath natively?

No. Plain property access, data.results[0].name, covers most cases. You need a library such as jsonpath-plus only for wildcards, recursive descent or filters.

How do I open a JSON file that is too big for my editor?

Do not open it. Stream it. stream-json in Node, ijson in Python, jq --stream on the command line. Each reads the file piece by piece instead of loading it whole.

Can I flatten nested JSON to CSV without losing anything?

You can keep every value, but not every relationship. The hierarchy has to move into the column names or into repeated rows, and arrays of different lengths force a choice between padding and unwinding. If the structure matters more than the table, keep the JSON and query it instead.

Key takeaway

### Is there a maximum nesting depth in JSON.