Skip to content
JSON to CSV

6 min read

How to Convert JSON to CSV in Python (3 Ways)

Python ships with everything you need: json for parsing and csv for writing. For flat data, the conversion is five lines. For nested objects, pandas handles the flattening. This guide covers both, plus the edge cases that silently produce wrong output.

The stdlib approach: json + csv

The simplest case is a JSON file containing an array of flat objects — every value is a string, number, boolean, or null, with no nesting. Python's built-in csv.DictWriter handles this directly.

Input JSON
[
  {"id": 1, "name": "Ada Lovelace", "email": "[email protected]"},
  {"id": 2, "name": "Grace Hopper", "email": "[email protected]"},
  {"id": 3, "name": "Hedy Lamarr",  "email": "[email protected]"}
]
convert.py
import csv
import json

with open("data.json") as f:
    records = json.load(f)

with open("data.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)
Output CSV
id,name,email
1,Ada Lovelace,[email protected]
2,Grace Hopper,[email protected]
3,Hedy Lamarr,[email protected]

Three things to note. First, newline="" in the open call is required on Windows — without it, csv.writer adds extra blank lines between rows. Second, fieldnames=records[0].keys() takes columns from the first record only. If later records have extra keys, DictWriter raises ValueError by default. Third, json.load reads the entire file into memory, so this pattern works for files that fit in RAM — typically up to a few hundred megabytes.

Handling records with different keys

Real-world JSON is ragged. One record has a phone field, another doesn't. An API response adds a metadata key starting at record 50. If you take columns from records[0], every key that appears later is silently dropped.

The fix is to collect the union of all keys before writing:

convert_ragged.py
import csv
import json

with open("data.json") as f:
    records = json.load(f)

# Collect every key across all records — not just the first one
all_keys: list[str] = []
seen: set[str] = set()
for rec in records:
    for key in rec:
        if key not in seen:
            all_keys.append(key)
            seen.add(key)

with open("data.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=all_keys, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(records)

dict.fromkeys preserves insertion order while deduplicating — so columns appear in the order they are first encountered across all records. Missing values become empty cells automatically because DictWriter fills them with restval="" by default.

The pandas shortcut

If pandas is already in your stack, the conversion is two lines:

pandas_convert.py
import pandas as pd

df = pd.read_json("data.json")
df.to_csv("data.csv", index=False)

pd.read_json infers column types and handles ragged keys automatically. For flat data, this is the fastest way. The file goes through DataFrame, so you get filtering, sorting, and column selection for free before the export.

Nested JSON: use json_normalize

When records contain nested objects, csv.DictWriter writes Python's dict repr into the cell — {'city': 'London'} — which is useless in a spreadsheet. pd.json_normalize flattens nested objects into dot-notation columns like address.city and address.geo.lat.

pandas_nested.py
import json
import pandas as pd

with open("orders.json") as f:
    data = json.load(f)

df = pd.json_normalize(data, sep=".")

# Lists survive json_normalize untouched — join them or they
# appear as Python reprs like "['a', 'b']" in the CSV
for col in df.columns:
    if df[col].apply(lambda v: isinstance(v, list)).any():
        df[col] = df[col].apply(
            lambda v: "; ".join(map(str, v)) if isinstance(v, list) else v
        )

df.to_csv("orders.csv", index=False, encoding="utf-8-sig")

The one catch everyone hits: json_normalize flattens objects but ignores lists. A tags field containing ["math", "computing"] passes through untouched and gets written as the Python repr. The snippet above handles this by joining lists with a semicolon. For a deeper look at array strategies, see the nested JSON to CSV guide.

Skip the code entirely

For one-off conversions, paste your JSON into the free converter — it handles nesting, arrays, and ragged keys in the browser with no upload and no size limit.

Open the JSON to CSV converter

NDJSON (one object per line)

Log pipelines and streaming APIs often produce NDJSON (also called JSON Lines): one JSON object per line, no wrapping array. json.load fails on this format because the file is not valid JSON as a whole. Read it line by line instead:

ndjson_convert.py
import csv
import json

rows: list[dict] = []
with open("events.jsonl") as f:
    for line in f:
        line = line.strip()
        if line:
            rows.append(json.loads(line))

all_keys = list(dict.fromkeys(k for r in rows for k in r))

with open("events.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=all_keys)
    writer.writeheader()
    writer.writerows(rows)

The pattern is the same as before — collect keys from all records, then write. The only difference is parsing each line separately.

One-liner for the terminal

When you need a quick conversion in a shell script or CI pipeline, a Python one-liner reads from stdin and writes to stdout:

Shell
python3 -c "
import csv, json, sys
data = json.load(sys.stdin)
keys = list(dict.fromkeys(k for r in data for k in r))
w = csv.DictWriter(sys.stdout, fieldnames=keys)
w.writeheader()
w.writerows(data)
" < data.json > data.csv

This uses the union-of-keys pattern so it works on ragged data. Pipe it into head or column -t -s, for a quick preview.

Common pitfalls

  • Encoding. If the CSV will be opened in Excel, write with encoding="utf-8-sig". The BOM tells Excel the file is UTF-8; without it, accented characters and CJK text display as garbage.
  • Line endings. Python's csv module writes \r\n on all platforms when you pass newline="" to open. Omit that argument and you get double carriage returns on Windows.
  • Booleans and nulls. json.load maps JSON true / false to Python True / False, and csv.writer writes them title-cased. If your downstream system expects lowercase true, convert explicitly before writing.
  • Large numbers. JSON has no integer size limit, but Excel truncates numbers beyond 15 significant digits. If you are exporting IDs like 9007199254740993, quote them as strings in the CSV or they will round on import.
  • Commas in values. Both csv.writer and csv.DictWriter handle RFC 4180 quoting automatically — values containing commas, quotes, or newlines are wrapped in double quotes. Never build CSV by hand-joining with commas.

Which approach to use

Use the stdlib json + csv when you need zero dependencies and the data is flat or nearly flat. Use pandas when the data is nested, you want type inference, or you need to filter and reshape before exporting. Use the browser converter when you want the result in ten seconds without writing any code.

Going the other direction? The CSV to JSON converter reads the CSV back and rebuilds nested objects from dot-notation headers.

JSON to CSV in Python: common questions

Does Python's csv module handle nested JSON?
No. csv.DictWriter expects flat dictionaries — one scalar value per key. If a value is a dict or list, Python writes its repr (e.g. "{'city': 'London'}") into the cell. Flatten nested structures before passing records to DictWriter, or use pandas json_normalize which handles nested objects automatically.
How do I handle a JSON file that is too large for memory?
If the file is a JSON array, you need to load it entirely because json.load must parse the full structure. For large data, use NDJSON (one JSON object per line) instead — you can read and write one line at a time. The ijson library also supports streaming a large JSON array without loading it all at once.
What is the difference between json.load and json.loads?
json.load reads from a file object; json.loads reads from a string. When converting a JSON file to CSV, use json.load with an open file handle. Use json.loads when you already have the JSON as a string variable, for example from an API response body.
How do I keep leading zeros in numeric columns when opening the CSV?
The CSV file itself preserves leading zeros correctly — the problem is Excel stripping them on import. Either import via Data → From Text/CSV and set those columns to Text, or add a UTF-8 BOM (encoding="utf-8-sig") which nudges Excel to handle the file more carefully. The converter on this site has a BOM toggle for exactly this case.
Should I use csv.writer or csv.DictWriter?
Use DictWriter when your records are dictionaries (the typical shape from json.load). DictWriter maps column headers to dict keys automatically. Use csv.writer only when you have data as plain lists of values and manage column order yourself.
How do I convert multiple JSON files to CSV at once?
Use pathlib.glob to iterate over files: for path in Path(".").glob("*.json"). You can write each to its own CSV, or merge all records into one file if they share the same schema. Collect the union of keys across all files before writing so no columns are missing.