JSON is hierarchical: it can nest objects inside objects, arrays inside arrays, many levels deep. CSV is flat, a grid of rows and columns. Converting between them is non-trivial, and naive approaches silently discard nested data. This guide explains how to do it correctly.
Why JSON-to-CSV Conversion Is Non-Trivial
A simple array of flat objects converts to CSV almost directly. But real-world JSON almost always contains nested structures:
[
{
"id": 1,
"name": "Alice",
"address": {
"city": "San Francisco",
"state": "CA"
},
"tags": ["developer", "admin"]
}
]The `address` object and `tags` array cannot fit directly into a flat CSV row. You must decide how to represent them, and the wrong choice means silently losing data.
Strategy 1: Dot-Notation Flattening
The most common and lossless approach for nested objects is to flatten keys using dot notation. The nested structure becomes part of the column name:
id,name,address.city,address.state,tags
1,Alice,San Francisco,CA,"[""developer"",""admin""]"This is lossless for objects and keeps the CSV importable. Arrays are serialized as a JSON string within the cell (not ideal for spreadsheets, but it preserves all data).
Strategy 2: Array Expansion (One Row Per Item)
When an array contains objects (common in API responses), you can expand it so each array item becomes its own row:
[
{
"orderId": "A001",
"items": [
{ "sku": "X1", "qty": 2 },
{ "sku": "Y9", "qty": 1 }
]
}
]Expanded output (the orderId is duplicated for each item row):
orderId,items.sku,items.qty
A001,X1,2
A001,Y9,1Strategy 3: Join Arrays as Strings
For arrays of primitive values (strings, numbers), joining them into a delimited string is simple and readable:
id,name,tags
1,Alice,"developer|admin"💡 Tip
Avoid commas as array delimiters within CSV cells since they conflict with CSV field separation. Use `|`, `;`, or serialize as a JSON array string instead.
Writing a Flattener in JavaScript
function flattenObject(obj, prefix = '') {
return Object.keys(obj).reduce((acc, key) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Recurse into nested objects
Object.assign(acc, flattenObject(value, fullKey));
} else if (Array.isArray(value)) {
// Serialize arrays as JSON strings
acc[fullKey] = JSON.stringify(value);
} else {
acc[fullKey] = value;
}
return acc;
}, {});
}
// Convert array of JSON objects to CSV string
function jsonToCsv(jsonArray) {
const flat = jsonArray.map(row => flattenObject(row));
const headers = [...new Set(flat.flatMap(Object.keys))];
const rows = flat.map(row =>
headers.map(h => {
const val = row[h] ?? '';
// Escape values containing commas or quotes
return typeof val === 'string' && (val.includes(',') || val.includes('"'))
? `"${val.replace(/"/g, '""')}"`
: val;
}).join(',')
);
return [headers.join(','), ...rows].join('\n');
}Don't want to write the code yourself?
Our JSON to CSV converter does all of this automatically. Paste your JSON and get a properly flattened CSV file with dot-notation headers.
Using Libraries
Several well-maintained npm packages handle JSON-to-CSV conversion with more options than a hand-rolled solution:
- json2csv: The most popular option. Supports nested flattening, custom headers, transforms, and streaming large datasets. Install: `npm install json2csv`.
- Papa Parse: Primarily a CSV parser, but also supports JSON-to-CSV conversion with excellent browser compatibility. Zero dependencies.
- flat: A utility package that just does object flattening (dot notation). Useful when you want to control CSV generation separately.
Edge Cases to Watch For
- Inconsistent schemas: If not all JSON objects have the same keys, your CSV headers must be the union of all keys, with empty cells for missing values.
- Deeply nested objects (3+ levels): Column names like `user.address.geo.lat` become unwieldy but remain valid.
- Null values: Represent as empty string in CSV, not the string `null`.
- Unicode and special characters: Ensure your CSV writer uses UTF-8 encoding, especially for names with accents or CJK characters.
- Boolean values: CSV has no boolean type; use `true`/`false` strings or `1`/`0` depending on your target system.
- Large numbers: JavaScript's `Number` type loses precision beyond 2^53. Use strings for large IDs (e.g., Twitter snowflake IDs).