Short answer: JSON converts cleanly to CSV when it is an array of flat objects — each object becomes a row, each key becomes a column. In Python it is ten lines with the csv module, and on the command line it is a single jq invocation. The only real work is nested data, which CSV cannot represent directly.
The shape that converts cleanly
CSV is two-dimensional: rows and columns, nothing else. The JSON shape that maps onto it without decisions is an array of objects with the same flat keys:
[
{ "id": 1, "name": "Alice", "role": "admin" },
{ "id": 2, "name": "Bob", "role": "editor" }
]
// becomes:
// id,name,role
// 1,Alice,admin
// 2,Bob,editorPython: csv.DictWriter
For flat records, the standard library is all you need. This assumes the data is a list of dicts with uniform keys:
import json, csv
with open("data.json") as f:
data = json.load(f)
with open("out.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)Nested data: pandas json_normalize
When objects contain objects, flatten them first. json_normalize turns nested keys into dot-notation columns:
import pandas as pd
data = [
{"id": 1, "user": {"name": "Alice", "city": "NYC"}},
{"id": 2, "user": {"name": "Bob", "city": "SF"}},
]
df = pd.json_normalize(data, sep=".")
df.to_csv("out.csv", index=False)
# id,user.name,user.city
# 1,Alice,NYC
# 2,Bob,SFThe jq one-liner
On the command line, jq handles the whole job — headers included:
jq -r '(.[0] | keys_unsorted) as $k | $k, map([.[ $k[] ]])[] | @csv' data.json > out.csvThe traps
- Nested objects don't fit: CSV has no nesting. Flatten with json_normalize first, or the converter joins the object into a single unreadable cell.
- Non-uniform keys: if some records have fields others lack, collect every key across all records before writing — otherwise columns silently disappear.
- Commas inside values: a value like "Boston, MA" must be quoted. DictWriter, pandas, and @csv all do this automatically; a hand-rolled string join does not.
- Forgetting newline="" in Python: on Windows, the csv module without it writes a blank line after every row.
- Excel showing one column: in some locales Excel expects semicolons, not commas. If everything lands in a single column, the delimiter is the problem — not your data.
Try it
Paste your JSON into the JSON / YAML / CSV — it converts in your browser with nothing uploaded, and shows you exactly how nested fields get flattened.