How to view nested JSON clearly (and read it safely in code)
Make sense of deeply nested JSON: trace a value through its parents, expand only one branch, and access it safely with optional chaining or jq paths.
Real JSON is rarely flat. A response from a payment provider, a cloud API or a CMS often has objects inside arrays inside objects, five or six levels deep, and the value you want is buried somewhere in the middle. Reading it as a wall of indented text is slow and error-prone. This post covers two connected skills: reading a nested document without getting lost, and writing code that reaches into it without throwing an error when a branch is missing. The examples use one invoice payload throughout, so you can see the same path from three angles.
What a nested payload looks like in practice
Here is a trimmed invoice response of the kind a billing API might return. The customer is an object, the line items are an array of objects, and each line item has its own nested price and tax objects. Nothing about it is unusual, yet finding the tax rate on the second line item already means following five steps.
{
"invoice": {
"id": "inv_2041",
"customer": {
"name": "Northwind Ltd",
"billing": { "address": { "city": "Leeds", "country": "GB" } }
},
"lines": [
{
"sku": "SUB-PRO",
"price": { "amount": 4900, "currency": "GBP" },
"tax": { "rate": 0.2, "jurisdiction": "GB-VAT" }
},
{
"sku": "SETUP",
"price": { "amount": 15000, "currency": "GBP" },
"tax": null
}
]
}
}Describe the location as a path
The most useful habit with nested data is to name a value by its path from the root. The city of the billing address is invoice, customer, billing, address, city. The tax rate of the first line item is invoice, lines, index 0, tax, rate. Writing the path down, or at least saying it out loud, forces you to notice each array along the way, which is where most mistakes come from. Object keys are looked up by name, but array elements are looked up by position, and forgetting that a step is an array is why people end up with undefined.
Notice also that the second line item has "tax": null. That is a valid value, distinct from the key being absent. Any path that goes through tax for that item has to cope with null, or your code will fail on the one record that differs from the others.
Use a tree to expand one branch at a time
A tree view turns the nesting into something you can fold. Each object and array becomes a node you can collapse, so you open invoice, then customer, then billing, and close everything else. The point is to remove noise, not to see more. In View My JSON, you paste the document or drop the file and open Tree view; the path you expand is the path you wrote down above. Parsing happens locally in your browser, and Tree, Cards and Table views are free.
Cards view suits a different shape of nesting: arrays of similar objects, like the lines array. Each line item gets its own card, so you compare SKU, price and tax across items without scrolling through indentation. Table view is the next step up if the items are flat enough to share columns. A good rule is to use Tree to learn the shape and Cards or Table to compare many records of that shape.
Reading nested values safely in JavaScript
The classic failure is a TypeError saying you cannot read properties of undefined or null. It happens when one step in the chain is missing and you keep going anyway. Optional chaining, written ?., stops the chain and returns undefined instead of throwing, and the nullish coalescing operator ?? supplies a default. Both are standard in modern JavaScript and TypeScript.
Be careful with one detail. Optional chaining guards against null and undefined only. If a value has the wrong type, say a string where you expected an object, the chain will not save you, and you still need a validation step for data that comes from outside your own code.
const inv = JSON.parse(text).invoice
// Fragile: throws on the second line item, where tax is null
const bad = inv.lines[1].tax.rate
// Safe: undefined if any step is missing or null
const rate = inv.lines[1]?.tax?.rate
// With a default when the value is absent
const rateOrZero = inv.lines[1]?.tax?.rate ?? 0
// City, even if the customer has no billing address on file
const city = inv.customer?.billing?.address?.city ?? 'unknown'Pulling values out with jq paths
On the command line, jq expresses the same paths with dots and brackets, and it is forgiving about missing keys: a path that does not exist yields null rather than an error. The trailing question mark and the // operator handle odd types and defaults. Use the iterate form, [], to apply a path to every element of an array.
If you do not know the structure at all, jq can tell you. The paths filter lists every path in the document, which is a quick way to discover where something lives before you write the real query. The recursive descent operator, two dots, finds a key at any depth, which is handy when an API wraps the data you want under a different parent each time.
# Tax rate on the first line item
jq '.invoice.lines[0].tax.rate' invoice.json
# SKU and tax rate for every line, treating null tax as 0
jq -c '.invoice.lines[] | {sku, rate: (.tax.rate // 0)}' invoice.json
# List every scalar path in the document
jq -r 'paths(scalars) | map(tostring) | join(".")' invoice.json
# Find every "city" key at any depth
jq '.. | objects | select(has("city")) | .city' invoice.jsonNesting traps that cause real bugs
The first trap is mixed shapes. An API may return an object when there is one result and an array when there are several, or return an empty string where it normally returns an object. Check what your sample records look like at each level, not just the first one. The second trap is null versus missing: some systems send "tax": null, others leave the key out, and a few do both depending on the code path. Handle both. The third is numbers: JSON has no integer type, and JavaScript parses every number as a double, so integers above 9,007,199,254,740,991 lose precision. Large identifiers are safer as strings.
A fourth, quieter trap is duplicate keys. RFC 8259 says object names SHOULD be unique, and parsers handle duplicates differently, so the value you see in one tool may not be what another one reads. If a nested value looks wrong, check the raw text for a repeated key before assuming a logic bug.
A workflow that holds up on unfamiliar payloads
When you meet a new nested response, start by validating it, so you know the text is well formed and any problem is about the structure, not the syntax. Open the tree, collapse everything, and expand one branch at a time until you find the value you need. Write its path down. Look at two or three other records at that same path to see whether it is always present and always the same type. Only then write the access code, with optional chaining or a jq filter that takes the missing cases into account.
That sequence takes a few minutes and prevents the usual outcome, which is code that works on the example you tested and fails in production on the first record with a null where you expected an object. The viewer is for building understanding. The path, and the defensive access code you write from it, is what you keep.