JSON to CSV: How to Handle Nested Objects and Arrays
· 4 min read
To convert nested JSON to CSV, flatten each object so nested keys become dot-notation columns (address.city), then decide how arrays should be represented: joined into one cell, spread into numbered columns, or exploded into one row per item. The JSON to CSV converter flattens objects automatically and runs in your browser.
Why nested JSON does not fit CSV
CSV is a flat grid: one row per record, one value per cell. JSON is a tree. A record like this has an object inside it and a list inside that:
[
{
"id": 1,
"name": "Ada",
"address": { "city": "London", "zip": "N1" },
"tags": ["admin", "dev"]
},
{
"id": 2,
"name": "Linus",
"address": { "city": "Portland" },
"tags": []
}
]Every conversion has to answer two questions: what are the columns, and what goes in a cell when the value is not a scalar.
Step 1: flatten objects into dot-path columns
Nested objects are the easy part. Walk the tree and join keys with a dot. Collect the union of keys across all records, because the second record has no address.zip:
id,name,address.city,address.zip,tags
1,Ada,London,N1,"[""admin"",""dev""]"
2,Linus,Portland,,[]Missing values become empty cells. Note that an empty cell cannot distinguish a missing key, an empty string and null; if that difference matters downstream, keep JSON or use an explicit marker.
Step 2: choose an array strategy
| Strategy | tags output | Good for | Downside |
|---|---|---|---|
| JSON in cell | ["admin","dev"] | Lossless round trips | Hard to read or filter in a spreadsheet |
| Join | admin; dev | Human-readable reports | Ambiguous if items contain the separator |
| Index columns | tags.0, tags.1 | Short arrays of known length | Column count grows with the longest array |
| Explode rows | One row per tag | Pivot tables, SQL import | Repeats the parent fields on each row |
Arrays of objects, such as order line items, are usually best exploded: one row per item, with the parent ID repeated so you can join back. If you only need part of the document, extract it first with the JSONPath tester, for example $.orders[*].items[*].
Doing it in code
Python with pandas
import json
import pandas as pd
with open("users.json") as f:
data = json.load(f)
df = pd.json_normalize(data) # address.city, address.zip, tags
df.to_csv("users.csv", index=False)
# One row per tag, keeping id and name
tags = pd.json_normalize(data, record_path="tags", meta=["id", "name"])json_normalize flattens nested objects with a . separator by default (sep changes it) and leaves lists as Python lists in the cell. record_path explodes a list into rows; note that records with an empty list produce no rows.
jq on the command line
jq -r '
["id","name","city","tags"],
(.[] | [.id, .name, .address.city, (.tags | join("; "))])
| @csv' users.json@csv quotes strings and escapes embedded quotes for you; -r prints raw text instead of JSON strings. You pick the columns explicitly, which is verbose but predictable.
JavaScript
function flatten(obj, prefix = "", out = {}) {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
else out[key] = Array.isArray(v) ? JSON.stringify(v) : v;
}
return out;
}
const rows = data.map((r) => flatten(r));
const cols = [...new Set(rows.flatMap(Object.keys))];
const esc = (v) => (v == null ? "" : /[",\n\r]/.test(String(v))
? `"${String(v).replace(/"/g, '""')}"` : String(v));
const csv = [cols.join(","), ...rows.map((r) => cols.map((c) => esc(r[c])).join(","))].join("\r\n");CSV pitfalls that cause real bugs
- Quoting. Per RFC 4180, a field containing a comma, quote or line break must be wrapped in double quotes, and quotes inside are doubled (
""). Hand-writtenjoin(",")breaks on the first address with a comma. - Formula injection. Spreadsheet apps may treat a cell beginning with
=,+,-or@as a formula. If the CSV contains user-supplied text and will be opened in Excel or Google Sheets, prefix such cells with a single quote. The JSON to CSV converter does this by default and leaves plain numbers like-12.5alone. - Encoding. Excel on Windows may misread UTF-8 without a byte order mark, turning accented characters into mojibake. Add a BOM or import via Data > From Text/CSV.
- Leading zeros and long numbers. Spreadsheets turn
00123into123and long IDs into scientific notation. The CSV is fine; the app's import is not. Import those columns as text. - Delimiter. Some locales expect
;because,is the decimal separator.
Going back: CSV to JSON
Converting back with the CSV to JSON tool gives you strings unless types are inferred, and dot-path headers are not automatically re-nested by every converter. If you need a faithful round trip, keep nested values as JSON text in the cell and parse them on the way back.
FAQ
How do I convert nested JSON to CSV?
Flatten nested objects into dot-notation columns such as address.city, then join, index or explode arrays. A converter like JSON to CSV does the flattening for you.
How do I convert JSON to CSV in Python?
Load it with json.load, flatten it with pandas.json_normalize, then call to_csv("out.csv", index=False).
How are arrays handled when converting JSON to CSV?
Common choices are keeping the array as JSON text in one cell, joining the items with a separator, numbered columns (tags.0, tags.1) or one row per item.
Can I open the CSV in Excel safely?
Yes, if cells that start with =, +, - or @ are neutralized and the file is UTF-8 with a BOM or imported through Excel's text import.
Why does my converted CSV have empty cells?
Not every record has every key. Columns are the union of all keys, so records without a key get an empty cell.

