Json To Csv LogoJson To Csv

How to Flatten JSON to CSV: What the Toggle Actually Does (2026 Guide)

Flattening turns nested JSON into spreadsheet-shaped rows: objects become dot-notation columns, lists join or stringify, empties follow exact rules. Here is what the toggle actually does, cell by cell, verified against the real engine.

Updated: 2026-10-1311 min read

Why JSON resists flatness: trees versus grids

A CSV is a grid: rows and columns, every cell a single flat value. JSON is a tree: objects inside objects, lists inside objects, objects inside lists. Flattening is the translation between those two shapes, and it is the one part of JSON-to-CSV conversion where a converter has to make judgment calls on your behalf — because there is no single \"correct\" answer.

Take the simplest nested record: { \"id\": 1, \"customer\": { \"name\": \"Ana\", \"city\": \"Madrid\" } }. A grid has no way to hold a \"customer\" that contains two things. So the converter must choose: give customer its own column and stuff the object in as text, or explode the object into customer.name and customer.city columns. Every converter makes this choice with the same handful of rules — and once you know them, you can predict your CSV before you ever press convert.

The running example we will flatten

Everything below is demonstrated on one file — three orders with three levels of nesting, a plain-value list, an empty list, an empty object, a null, and a list of objects. We ran this exact file through this site's real conversion engine (the same JavaScript that runs when you press Convert), and every output shown is what the engine actually produced:

orders-nested.json
[
  { "orderId": "A-101", "customer": { "name": "Ana Ruiz", "contact": { "email": "ana@example.com", "city": "Madrid" } }, "items": ["book", "pen"], "tags": [], "meta": {} },
  { "orderId": "A-102", "customer": { "name": "Ken Sato", "contact": { "email": "ken@example.com", "city": "Osaka" } }, "items": ["lamp"], "tracking": [{ "code": "TX-1", "status": "shipped" }, { "code": "TX-2", "status": "pending" }] },
  { "orderId": "A-103", "customer": { "name": "Lina Noor", "contact": { "email": null, "city": "Berlin" } }, "items": ["mug", null, "plate"] }
]

Rule 1: nested objects become dot-notation columns

The first and most important rule: every level of object nesting becomes a column name with the levels joined by dots. customer.contact.email is not three columns — it is one column whose name is the full path down the tree. Here is the engine's actual output with flattening on:

CSV output — flattening ON (verified against the real engine)
orderId,customer.name,customer.contact.email,customer.contact.city,items,tags,meta,tracking
A-101,Ana Ruiz,ana@example.com,Madrid,book; pen,,{},
A-102,Ken Sato,ken@example.com,Osaka,lamp,,,"[{""code"":""TX-1"",""status"":""shipped""},{""code"":""TX-2"",""status"":""pending""}]"
A-103,Lina Noor,,Berlin,mug; ; plate,,,

Nesting depth has no practical limit in the rule itself — we also tested a four-level object ({ a: { b: { c: { d: \"deep\" } } } }) and the engine produced a single column named a.b.c.d with the value deep. The dotted names sort and filter normally in a spreadsheet; you can rename the headers afterward in one pass if customer.contact.city is too long for comfort.

Rule 2: lists of plain values join into one cell

When a list contains only plain values — strings, numbers, booleans, nulls — the whole list collapses into a single cell with the values joined by \"; \" (semicolon-space). Look at the items column above: [\"book\", \"pen\"] became the single cell book; pen, and [\"mug\", null, \"plate\"] became mug; ; plate — the null turned into an empty segment, not a skipped one.

Two practical consequences. First, if you later pick a semicolon as your CSV delimiter, that joined cell gets quoted automatically (\"book; pen\") so the separator inside the cell cannot split it — the engine applies CSV quoting rules per cell, verified in the test above. Second, joining loses the boundary information: after flattening, book; pen is text, not a list. If you need to split items back into rows, see Rule 3.

Rule 3: lists of objects collapse into one JSON-text cell

This is the rule that surprises people. When a list contains objects — like the tracking array with its { code, status } entries — it cannot become columns (objects have structure, not just values) and it cannot join like plain values. So the engine stores the whole list as a quoted JSON-text cell. Above, the tracking column for A-102 holds the full JSON array as text, escaped per CSV rules.

Is that a limitation? Honestly: yes, for this shape. No row-expansion toggle exists in this converter — it never creates one CSV row per list item. If you actually need one row per tracking event, restructure before converting: pull the inner array out (convert [{ \"code\": \"TX-1\", \"status\": \"shipped\" }, ...] on its own) and you get clean code/status rows. One column of JSON text is the honest alternative to silently inventing rows you did not ask for.

Rule 4: empties and nulls follow exact rules

Nulls and empties are where most \"why is my CSV weird\" confusion starts, so here are the verified behaviors, no guessing:

  • A null value becomes an empty cell. The email: null for Lina Noor produced an empty cell under customer.contact.email — no \"null\" text, no quotes.
  • An empty list becomes an empty cell. The tags: [] for A-101 left the cell blank. (Note: this is the flatten-ON behavior; with flattening off the same list appears as the literal text [].)
  • An empty object becomes the text {}. The meta: {} for A-101 produced a cell containing {} — the engine has nothing to expand, so it writes the object's literal shape.
  • A null inside a list becomes an empty joined segment — mug; ; plate above — rather than being skipped or written as \"null\".

These are design decisions, not universal law, but they are consistent: null means \"nothing here\", and the CSV says nothing.

Flattening ON versus OFF, byte for byte

Same file, flattening off. Every top-level key keeps its own column and nested structures are stored as quoted JSON text:

CSV output — flattening OFF (verified against the real engine)
orderId,customer,items,tags,meta,tracking
A-101,"{""name"":""Ana Ruiz"",""contact"":{""email"":""ana@example.com"",""city"":""Madrid""}}","[""book"",""pen""]",[],{},
A-102,"{""name"":""Ken Sato"",""contact"":{""email"":""ken@example.com"",""city"":""Osaka""}}","[""lamp""]",,,"[{""code"":""TX-1"",""status"":""shipped""},{""code"":""TX-2"",""status"":""pending""}]"
A-103,"{""name"":""Lina Noor"",""contact"":{""email"":null,""city"":""Berlin""}}","[""mug"",null,""plate""]",,,

Notice the off-mode details: the columns shrink to the top-level keys (customer, items, tags, meta, tracking), each nested value is stringified raw JSON with CSV quote-doubling, and the empty list and empty object appear as literal [] and {} text. Off is the honest-but-ugly representation — correct when you need to preserve the exact nested structure (for example, before feeding rows back into a system that expects JSON-shaped fields). For analysis — filters, pivot tables, charts — flattening on is the right call, which is why it is the default.

Where the rows come from: arrays, API wrappers, and JSON Lines

Flattening decides the columns; something else decides the rows. The engine looks at your top-level value and follows three cases:

Case one: your top level is an array of objects — each object becomes one row. This is the orders example above: three objects, three rows. Case two: your top level is a single object — you get one row. Case three is the useful one: your top level is an object that wraps an array, the way API responses do — { \"data\": [ ... ], \"total\": 2 }. The engine detects the inner array (it checks common wrapper keys like data, results, users, items) and makes each element a row, dropping the wrapper fields. We verified this: { \"data\": [{ \"a\": 1, \"b\": 2 }, { \"a\": 3, \"b\": 4 }], \"total\": 2 } produced exactly the two data rows with columns a and b — total gone.

One more input shape worth knowing: the engine also accepts JSON Lines (NDJSON) — one JSON object per line. If your export is ten thousand lines instead of one big array, paste it as-is; it converts the same way.

Ragged data: missing keys just leave empty cells

Real exports are ragged: order A-102 has tracking but no meta, A-101 has meta but no tracking, A-103 has neither. The engine builds the column list as the union of every key found in every row, in order — so the header row above has all eight columns — and any row missing a key simply gets an empty cell under it.

This is why the headers come out in insertion order (orderId first, then the first-seen nested keys) rather than alphabetical order: the columns follow the shape of your data as the engine walks it. If a later row introduces a brand-new nested path, the column appears anyway — nothing is silently dropped.

Three edge cases that bite

First: keys that already contain dots. If your JSON has both a literal key \"user.name\" and a nested user: { name: ... }, both want the same column. We tested it: the nested value wins and the literal key's value is lost ([{ \"user.name\": \"x\", user: { name: \"y\" } }] produced one column user.name containing y). Rename dotted keys before converting if both values matter.

Second: mixed-type lists. A list like [1, \"a\", { \"b\": 2 }] is not \"all primitives\", so the engine treats it like an object list and stringifies the whole thing into one JSON-text cell — even though most of it was plain. Keep lists homogeneous if you want the \"; \"-join behavior.

Third: deep nesting is fine, but watch the column names. Ten levels of nesting gives you a ten-part dotted column name, which is technically correct and practically unreadable. Flatten everything, then rename the three columns you actually care about.

The 30-second flattening checklist

If you remember nothing else: leave flattening on for analysis, turn it off only to preserve structure; expect dot-notation columns and \"; \"-joined lists; pull object-lists out and convert them separately when you need one row per item; and treat empty cells as the honest representation of nulls — not as errors. The toggle is one click, but now you know exactly what that click does.

Related guides

Frequently asked questions

What does flattening JSON to CSV mean?

Flattening is the step that turns nested JSON into a flat spreadsheet grid: nested objects become dot-notation columns (customer.contact.city), lists of plain values join into one cell with "; ", and lists of objects collapse into a single JSON-text cell. Without flattening, nested values are stored as raw quoted JSON text in one column each.

How do I flatten nested JSON objects into CSV columns?

Use a converter with a flattening option and leave it on — nested objects are expanded into dot-notation columns automatically, at any depth (customer.contact.email, or even a.b.c.d). On this site's converter, flattening is the default: paste or upload your JSON, convert, and each nested path becomes its own sortable, filterable column.

What happens to arrays when flattening JSON to CSV?

It depends on what the array holds. Lists of plain values (strings, numbers) join into a single cell with "; " — [\"book\", \"pen\"] becomes book; pen. Lists of objects are stored as one quoted JSON-text cell, because objects cannot become flat values. Empty lists become empty cells. Mixed lists are stringified whole, so keep lists homogeneous for predictable results.

Why does my flattened CSV have so many empty cells?

Empty cells are normal and honest: a null value becomes an empty cell, an empty list becomes an empty cell, and when rows have different keys (ragged data) the missing keys leave blanks. Check whether the empties line up with nulls in your source JSON — if they do, the conversion is correct.

Should I turn flattening on or off when converting JSON to CSV?

Leave it on for analysis — dot-notation columns are what pivot tables, filters, and charts need. Turn it off only when you must preserve the exact nested structure, e.g. before feeding rows back into a system that expects JSON-shaped fields. Off mode stores each nested value as quoted raw JSON text, which is correct but unusable for spreadsheets.

Can I get one CSV row per item in a nested array of objects?

This converter does not expand object-lists into rows — it stores them as one JSON-text cell. The reliable workaround: extract the inner array and convert it on its own (e.g. convert just [{\"code\":\"TX-1\"},{\"code\":\"TX-2\"}]) to get clean one-row-per-item output. Do the extraction before converting, not after.

Is there a free tool that flattens JSON to CSV online?

Yes — this site's converter is free, needs no sign-up, and flattens nested JSON by default (dot-notation columns, \"; \"-joined lists) entirely in your browser, so your data never leaves your device. Upload a file or paste JSON, leave flattening on, pick your delimiter, and download the CSV.

Have nested JSON? Flatten it now — nothing to install

Paste your nested JSON or upload the file: objects become dot-notation columns, lists join cleanly, and the conversion runs entirely in your browser. Free, no sign-up.

Open the JSON to CSV converter
All guides