Parsing Nested JSON in JavaScript
Most real API responses are not flat. You get an object that holds an array of objects, each holding another object, and somewhere five levels down is the one value you actually need. Reaching it without crashing on a missing key is where most JavaScript bugs around JSON come from. This guide walks through the patterns that hold up against messy, inconsistent data.
Start by Seeing the Shape
You cannot reliably access what you cannot see. A minified API response printed to the console is unreadable, and console.log collapses deep objects into [Object] once they pass a certain depth.
Before writing any access code, format the response so the structure is obvious:
Paste a raw response into the JSON viewer to get a collapsible tree, or the formatter for indented text. Seeing that geo is null on the second address before you write code saves you from a runtime error later. That single null is the kind of thing that breaks naive access.
Accessing Nested Values
Once you have parsed the string with JSON.parse, nested values are reached with dot or bracket notation:
Bracket notation is required when a key has spaces, hyphens, or starts with a number:
This works right up until a key in the chain is missing. res.data.user.addresses[1].geo.lat throws TypeError: Cannot read properties of null (reading 'lat') because the work address has geo: null. One bad record takes down the whole function.
Optional Chaining Is the Default Now
Optional chaining (?.) short-circuits to undefined the moment any link in the chain is null or undefined, instead of throwing:
Pair it with the nullish coalescing operator (??) to supply a fallback only when the value is null or undefined, not when it is 0 or an empty string:
The difference between ?? and || matters here. If a valid latitude is 0, || 0 would replace it pointlessly, while ?? 0 keeps it. Reach for ?? whenever 0, false, or "" are legitimate values.
Looping Over Nested Arrays
The common task is pulling one field out of every object in a nested array. Use map for that:
When the array itself might be absent, guard it so you do not call .map on undefined:
Defaulting to an empty array means downstream code always gets an array, never a crash. For deeper extraction, combine flatMap to flatten one level:
Filtering out the null geo first means the flatMap callback never touches a null.
Pulling One Value Out of a Deep Tree
Sometimes you do not know the exact path, or you want every matching value regardless of where it sits. Writing nested loops for that gets ugly fast. JSONPath expresses it in a single line:
The recursive descent operator .. walks every level, which is handy when an API nests the same field in different places. You can prototype these against your real data in the JSONPath evaluator before porting the logic into code, then implement the confirmed path with plain accessors.
Choosing an Approach
| Situation | Use |
|---|---|
| Known path, value always present | Dot or bracket notation |
| Known path, links may be missing | Optional chaining ?. with ?? |
| One field from an array of objects | map over a defaulted array |
| Unknown depth or scattered matches | JSONPath recursive descent |
| Exploring an unfamiliar response | JSON viewer tree first |
A Reusable Safe-Get Helper
If you are stuck supporting an older runtime without optional chaining, or you want a path as a string, a small helper covers it:
The acc == null check uses loose equality on purpose so it catches both null and undefined in one comparison. For anything modern, prefer native optional chaining; it is faster and reads better than passing string paths around.
The Gotcha That Bites Everyone
JSON.parse gives you plain objects and arrays, nothing more. Dates come back as strings, not Date objects, and a number that exceeds Number.MAX_SAFE_INTEGER (such as a 64-bit ID) loses precision silently. If your API returns "id": 9007199254740993, JavaScript reads it as 9007199254740992. When IDs matter, ask the API to send them as strings, and parse those long numeric fields yourself rather than trusting JSON.parse to keep every digit.