JSON is how APIs usually hand data to developers. CSV is how analysts, spreadsheets, finance teams, and many import tools expect to receive it. The conversion looks simple until one record has an extra field, one value contains a comma, or Excel quietly turns a customer ID into a number.
The safe rule is this: convert JSON to CSV only after you decide how a JSON value should become a table. A CSV file has rows and columns. JSON can have objects, arrays, nested arrays, nested objects, booleans, nulls, and numbers that should sometimes stay strings. The hard part is not "write commas." The hard part is preserving intent.
This guide walks through the mapping I would use for real exports, including the same practical choices used by the JSON to CSV converter: first-seen header union, standard CSV quote escaping, nested JSON stored as cell text, BOM stripping on CSV input, and conservative CSV-to-JSON type coercion.
Quick Answer
To convert JSON to CSV:
- Parse or repair the JSON first.
- Normalize the input to records.
- Build the header row from the union of object keys.
- Write one CSV row per record.
- Convert nested objects and arrays to JSON text inside a cell, unless you intentionally flatten them.
- Quote any cell containing a comma, quote, carriage return, or newline.
- When converting CSV back to JSON, treat the first row as headers and coerce only obvious values.
The cleanest JSON shape is an array of objects:
[
{ "id": 1, "name": "Ada Lovelace", "active": true },
{ "id": 2, "name": "Grace Hopper", "active": false }
]
That maps naturally to:
id,name,active
1,Ada Lovelace,true
2,Grace Hopper,false
If your JSON is a single object, treat it as one row. If it is an array of primitive values, use one column such as value. If it is deeply nested, decide whether you want reversible output or spreadsheet-friendly output before you convert.
The Core Mapping: JSON Values to CSV Rows
CSV is a flat table. JSON is a tree. So every converter has to choose a table shape.
| JSON input shape | Sensible CSV output | Notes |
|---|---|---|
| Object | One row | Keys become columns |
| Array of objects | One row per object | Best default for API exports |
| Array of strings/numbers | One value column |
Useful for simple lists |
| Nested object in a field | JSON text inside one cell, or flattened columns | Pick based on round-trip needs |
| Array inside a field | JSON text inside one cell, or one-to-many rows | CSV has no native nested list |
| Mixed array | Avoid if possible | Normalize before export |
For most application data, aim for an array of objects:
[
{
"id": 1001,
"email": "ada@example.com",
"plan": "pro"
},
{
"id": 1002,
"email": "grace@example.com",
"plan": "team",
"trial": false
}
]
The header should include every key that appears in any object, not only the keys in the first row:
id,email,plan,trial
1001,ada@example.com,pro,
1002,grace@example.com,team,false
That blank trial cell in the first row is intentional. It means the key was missing or the value was empty in that record. Dropping the column would be worse, because then the second row loses data.
Header Strategy: Union, Order, and Missing Cells
There are three common header strategies:
| Strategy | What it does | When to use it |
|---|---|---|
| First record only | Uses Object.keys(rows[0]) |
Only when every record is guaranteed to have the same keys |
| Union of all keys | Adds keys as they first appear | Good default for messy API data |
| Predefined schema | Uses a fixed column list | Best for production exports and imports |
The browser converter uses the union approach in first-seen order. That means the first object sets the initial column order, and later objects can append new columns.
const headers = [];
const seen = new Set();
for (const row of rows) {
for (const key of Object.keys(row)) {
if (!seen.has(key)) {
seen.add(key);
headers.push(key);
}
}
}
For one-off exports, this is friendly because it does not silently drop late-appearing fields. For production pipelines, I prefer a predefined schema because it catches typos:
const headers = ['id', 'email', 'plan', 'trial'];
If a producer suddenly sends emial instead of email, a union-based export will create a new column. A schema-based export can fail loudly.
CSV Quoting Rules You Cannot Skip
CSV escaping is where most hand-written converters break. A value must be quoted when it contains:
- A comma
- A double quote
- A carriage return
- A newline
Inside a quoted field, a double quote is escaped by doubling it.
| Raw value | CSV cell |
|---|---|
Ada Lovelace |
Ada Lovelace |
Ada, Countess of Lovelace |
"Ada, Countess of Lovelace" |
She said "hello" |
"She said ""hello""" |
line 1\nline 2 |
"line 1\nline 2" |
The conversion function is small, but this one helper is non-negotiable:
function escapeCsvCell(value) {
const text = value == null
? ''
: typeof value === 'object'
? JSON.stringify(value)
: String(value);
return /[",\n\r]/.test(text)
? `"${text.replace(/"/g, '""')}"`
: text;
}
Do not parse real CSV with split(','), and do not write CSV by joining raw values. Both work until the first customer name, address, note, or JSON-in-a-cell contains a comma.
JavaScript: JSON to CSV
This version matches the practical behavior most people expect:
- A single object becomes one row.
- An array of objects becomes many rows.
- An array of primitive values becomes a single
valuecolumn. - Object and array values inside cells are serialized as compact JSON.
nullandundefinedbecome blank cells.
function jsonToCsv(input) {
const rows = Array.isArray(input) ? input : [input];
const headers = [];
const seen = new Set();
for (const item of rows) {
if (item && typeof item === 'object' && !Array.isArray(item)) {
for (const key of Object.keys(item)) {
if (!seen.has(key)) {
seen.add(key);
headers.push(key);
}
}
}
}
if (headers.length === 0) {
return ['value', ...rows.map(escapeCsvCell)].join('\n');
}
const lines = [headers.map(escapeCsvCell).join(',')];
for (const item of rows) {
const objectRow = item && typeof item === 'object' && !Array.isArray(item)
? item
: {};
lines.push(headers.map((header) => escapeCsvCell(objectRow[header])).join(','));
}
return lines.join('\n');
}
Use it like this:
const csv = jsonToCsv([
{
id: 1,
name: 'Ada, Countess of Lovelace',
tags: ['math', 'notes']
}
]);
console.log(csv);
Output:
id,name,tags
1,"Ada, Countess of Lovelace","[""math"",""notes""]"
That nested tags array is still recoverable because it was written as JSON text inside one CSV cell.
JavaScript: CSV to JSON
The reverse direction starts by parsing a grid of cells. You need a quote-aware parser because quoted fields may contain commas and line breaks.
function parseCsvGrid(text) {
const rows = [];
let row = [];
let field = '';
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (inQuotes) {
if (char === '"' && text[i + 1] === '"') {
field += '"';
i++;
} else if (char === '"') {
inQuotes = false;
} else {
field += char;
}
continue;
}
if (char === '"') inQuotes = true;
else if (char === ',') {
row.push(field);
field = '';
} else if (char === '\n') {
row.push(field);
rows.push(row);
row = [];
field = '';
} else if (char !== '\r') {
field += char;
}
}
if (field !== '' || row.length) {
row.push(field);
rows.push(row);
}
return rows;
}
Then map the first row to object keys:
function csvToJson(csv) {
const text = csv.replace(/^\uFEFF/, '');
if (!text.trim()) return [];
const grid = parseCsvGrid(text);
const headers = grid[0];
return grid.slice(1).map((cells) => {
const row = {};
headers.forEach((header, index) => {
row[header] = coerceCsvValue(cells[index] ?? '');
});
return row;
});
}
The BOM strip matters because Excel-generated CSV files sometimes start with a UTF-8 byte-order mark. If you do not remove it, the first header can become "\uFEFFid" instead of "id".
Type Coercion: Keep It Conservative
CSV itself has no types. Every cell is text. JSON has booleans, numbers, strings, null, arrays, and objects. So CSV-to-JSON conversion has to decide whether a cell should stay text or become a JSON scalar.
The safest automatic rule is conservative coercion:
function coerceCsvValue(value) {
if (value === '') return '';
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) {
const numberValue = Number(value);
if (Number.isFinite(numberValue) && String(numberValue) === value) {
return numberValue;
}
}
return value;
}
This avoids several bad conversions:
| CSV cell | Conservative JSON value | Why |
|---|---|---|
42 |
42 |
Lossless number |
-3.5 |
-3.5 |
Lossless number |
true |
true |
Exact lowercase boolean |
null |
null |
Exact JSON null |
007 |
"007" |
Leading zero likely matters |
1e3 |
"1e3" |
String form does not round-trip after Number() |
9007199254740993 |
"9007199254740993" |
Too risky for JavaScript number precision |
| empty cell | "" |
Blank cell is not automatically null |
This is less magical than "dynamic typing everything," and that is the point. It keeps IDs, ZIP codes, SKU values, and large account numbers from being damaged.
Nested JSON: Cell Text vs Flattened Columns
When a JSON value contains an object or array, CSV cannot represent it directly. You have two honest options.
Option 1: Store nested JSON inside one cell
[
{
"id": 1,
"name": "Ada",
"address": {
"city": "London",
"postal": "SW1A 1AA"
}
}
]
id,name,address
1,Ada,"{""city"":""London"",""postal"":""SW1A 1AA""}"
This is the best choice when you may convert the CSV back to JSON later. It is not the prettiest spreadsheet, but it preserves the nested object.
Option 2: Flatten nested fields
id,name,address.city,address.postal
1,Ada,London,SW1A 1AA
This is easier for spreadsheet filtering and BI tools. It becomes awkward when arrays appear:
{
"id": 1,
"roles": ["admin", "editor"]
}
Should that become roles.0 and roles.1, a semicolon-separated cell, or two rows? There is no universal answer. Choose based on the tool that will read the CSV next.
Python: JSON to CSV and Back
Python's standard library is enough for most CSV work. Use csv.DictWriter for JSON-to-CSV when you already have a list of dictionaries.
import csv
import io
import json
rows = json.loads(json_text)
fieldnames = []
for row in rows:
for key in row.keys():
if key not in fieldnames:
fieldnames.append(key)
buffer = io.StringIO(newline='')
writer = csv.DictWriter(buffer, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
csv_text = buffer.getvalue()
For CSV-to-JSON:
import csv
import io
import json
reader = csv.DictReader(io.StringIO(csv_text))
rows = list(reader)
json_text = json.dumps(rows, indent=2)
Notice that Python's csv.DictReader returns strings. That is usually the right starting point. Add explicit per-column conversion after parsing if your schema says active is a boolean or quantity is an integer.
Excel and Google Sheets Gotchas
If the CSV is headed to a spreadsheet, test it there. Spreadsheet programs reinterpret text aggressively.
| Gotcha | What happens | Safer export |
|---|---|---|
| Long ID | 9007199254740993 may lose precision |
Import the column as text |
| Leading zero | 007 becomes 7 |
Keep as text |
| Date-like value | 2026-05-26 becomes locale-formatted date |
Import as text if exact string matters |
| Formula-looking cell | =SUM(A1:A2) may execute as a formula |
Escape or prefix as text before sharing |
| Unicode names | Non-ASCII text may render incorrectly in old Excel flows | Use UTF-8 and add a BOM for file downloads |
| Semicolon locale | Comma CSV opens as one column | Import with delimiter selection |
The online converter gives you text in the browser. If you are building a downloadable file for Excel, consider prepending a UTF-8 BOM and using the line endings your downstream system expects. For engineering pipelines, plain UTF-8 with comma delimiters is usually fine.
Common Mistakes
Mistake 1: Using split(',')
This row has three logical fields:
id,name,note
1,"Ada, Countess of Lovelace","line one
line two"
A naive comma split sees more than three pieces because it does not understand quotes. Use a parser that tracks quote state.
Mistake 2: Trusting the first object for headers
If the first object is missing a field, every later value for that field disappears.
[
{ "id": 1, "name": "Ada" },
{ "id": 2, "name": "Grace", "email": "grace@example.com" }
]
Use header union or a fixed schema.
Mistake 3: Coercing every numeric-looking string
007, 000123, 1e3, and 9007199254740993 often look numeric but should stay strings. Only coerce when the value round-trips exactly and your schema agrees.
Mistake 4: Treating blank as null
An empty CSV cell can mean "missing," "intentionally blank," or "unknown." Do not turn every blank into null unless your import contract says so.
Mistake 5: Forgetting duplicate headers
CSV with duplicate headers is ambiguous:
id,name,name
1,Ada,Lovelace
Most object-based converters can only keep one name value. Rename columns before converting, such as first_name,last_name.
Convert JSON to CSV Online
For one-off work, paste your JSON into JSON to CSV Converter and click To CSV. Paste CSV and click To JSON to go the other way.
The tool runs locally in your browser. For JSON-to-CSV, it first tries strict JSON parsing and then a repair pass for common JSON mistakes. For CSV-to-JSON, it uses the first row as headers, respects quoted commas and quoted newlines, strips a leading BOM, and performs conservative coercion for exact booleans, null, and lossless numbers.
That makes it useful for API exports, spreadsheet handoffs, and quick data cleanup. For scheduled production exports, put the same rules into code and add tests for the edge cases above.
Frequently Asked Questions
How do I convert JSON to CSV?
Start with a JSON array of objects, build the CSV header from the union of all object keys, then write one row per object. Quote cells containing commas, quotes, carriage returns, or newlines. Store nested objects and arrays as JSON text inside the cell unless you intentionally flatten them.
Can I convert a single JSON object to CSV?
Yes. Treat the object as one row. Its keys become the header row and its values become the first data row. If the input is an array of primitive values instead, use one column such as value.
How do I convert CSV back to JSON?
Parse the CSV with a quote-aware parser, strip a leading UTF-8 BOM if present, use the first row as field names, and map each following row to an object. Coerce only safe values such as exact true, false, null, and lossless numbers.
How are nested JSON objects represented in CSV?
CSV cannot nest values. The reversible option is to serialize each nested object or array as compact JSON inside one cell. The spreadsheet-friendly option is to flatten fields into columns such as address.city, but that can be lossy for arrays and irregular objects.
Why did my CSV change IDs, dates, or leading zeros?
The converter may have kept the value as text, but the spreadsheet app may reinterpret it after opening the file. Import sensitive columns as text when IDs, ZIP codes, long numbers, formula-looking strings, or date-like values must stay exact.
Related Tools and Guides
- JSON to CSV Converter - convert both directions in your browser.
- Convert JSON to CSV: Array of Objects - the shorter implementation guide.
- How to Convert CSV and XML to JSON - CSV import plus XML shape decisions.
- JSON to XML - convert the same JSON to a markup format.
- What Is JSON? - the data types behind the conversion.
- JSON Validator - check your JSON before converting.
Sources
- RFC 4180 - common CSV format rules, including quoted fields.
- RFC 8259 - the JSON Data Interchange Format.
- Python csv module - standard-library CSV parsing and writing.
- PapaParse - robust CSV parsing for JavaScript.
- json2csv - JSON-to-CSV conversion for JavaScript and Node.
Last reviewed July 2026.