DataShift
·11 min read

How to Convert JSON to CSV in Python: The Complete Guide

Convert JSON to CSV in Python using the standard library, pandas, or an online tool. Covers flat JSON, nested objects, arrays, and real API data — with working code for each approach.

You've pulled data from an API, exported records from a database, or received a .json file from a colleague. Now you need it in CSV so you can open it in Excel, load it into a SQL table, or feed it into a reporting tool.

Python is the most common way to do this. But the right approach depends on your data. A flat array of objects converts in three lines. A nested Stripe API response with arrays inside arrays? That takes a strategy.

This guide covers three approaches — the standard library, pandas, and an online tool — and shows you when to use each one. Every example uses real-world data structures from actual APIs, not toy {"name": "Alice"} snippets.

When You Need JSON to CSV

JSON is hierarchical. CSV is flat. That's the core tension. A JSON object can nest objects inside objects, store arrays of mixed types, and have fields that exist in some records but not others. CSV needs uniform columns and one value per cell.

You'll hit this conversion in a few common situations:

API exports. You pulled payment data from Stripe, contacts from HubSpot, or orders from Shopify. The response is JSON. Your finance team wants a spreadsheet.

Database dumps. MongoDB exports to JSON natively. Moving that data into PostgreSQL or MySQL means converting to a flat format first.

Data analysis. pandas works brilliantly with DataFrames. Getting your JSON into a DataFrame is step one of any analysis pipeline.

Sharing with non-technical teams. Your colleague doesn't know what JSON is. They know Excel. Converting to CSV is the bridge.

Method 1: Python Standard Library (json + csv)

No dependencies needed. This approach uses modules that ship with Python.

Flat JSON

If your JSON is an array of objects with no nesting, this is the simplest path:

import json
import csv

# Load JSON data
with open("users.json") as f:
    data = json.load(f)

# data looks like:
# [
#   {"id": 1, "name": "Alice", "email": "alice@example.com", "plan": "pro"},
#   {"id": 2, "name": "Bob", "email": "bob@example.com", "plan": "free"},
# ]

# Write to CSV
with open("users.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=data[0].keys())
    writer.writeheader()
    writer.writerows(data)

DictWriter maps each dictionary's keys to CSV columns. The newline="" parameter prevents blank rows on Windows. The encoding="utf-8" handles international characters.

JSON with inconsistent fields

Real-world JSON often has records where some fields are missing. DictWriter handles this if you collect all possible keys first:

import json
import csv

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

# Collect the union of all keys across all records
all_keys = set()
for record in data:
    all_keys.update(record.keys())

fieldnames = sorted(all_keys)

with open("contacts.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames, restval="")
    writer.writeheader()
    writer.writerows(data)

The restval="" parameter fills missing fields with empty strings instead of raising an error.

When to use this approach

Use the standard library when you don't want external dependencies — Lambda functions, Docker containers, CI scripts, or environments where installing pandas is overkill. It's also the right choice when your JSON is flat or nearly flat and you want maximum control over the output.

Method 2: pandas (read_json and json_normalize)

pandas is the Swiss Army knife for this task. It handles flat JSON, nested objects, arrays, and mixed schemas with functions purpose-built for the job.

Flat JSON

import pandas as pd

# From a file
df = pd.read_json("users.json")
df.to_csv("users.csv", index=False)

# From a JSON string
json_str = '[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]'
df = pd.read_json(json_str)
df.to_csv("users.csv", index=False)

Two lines. index=False prevents pandas from writing its row index as an extra column.

Nested JSON with json_normalize

This is where pandas earns its keep. json_normalize recursively flattens nested objects into columns with dot-separated names:

import pandas as pd
import json

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

# data contains Stripe-like payment objects:
# [
#   {
#     "id": "pi_3MtwBwLkdIwHu7ix",
#     "amount": 2000,
#     "currency": "usd",
#     "customer": {
#       "id": "cus_NffrFeUfNV2Hib",
#       "email": "jenny.rosen@example.com"
#     },
#     "payment_method": {
#       "type": "card",
#       "card": {"brand": "visa", "last4": "4242"}
#     }
#   }
# ]

df = pd.json_normalize(data, sep=".")
print(df.columns.tolist())
# ['id', 'amount', 'currency', 'customer.id', 'customer.email',
#  'payment_method.type', 'payment_method.card.brand',
#  'payment_method.card.last4']

df.to_csv("payments_flat.csv", index=False)

Every nested field becomes its own column. Three levels of nesting (payment_method.card.brand) flatten cleanly into a single column name.

Expanding arrays with record_path

When your JSON has arrays of objects — like order line items — json_normalize can expand each array element into its own row:

import pandas as pd

orders = [
    {
        "order_id": "5001",
        "customer_email": "buyer@example.com",
        "line_items": [
            {"sku": "SHOE-BLK-42", "quantity": 1, "price": 89.99},
            {"sku": "SOCK-GRY-M", "quantity": 3, "price": 7.99},
        ],
    },
    {
        "order_id": "5002",
        "customer_email": "shopper@example.com",
        "line_items": [
            {"sku": "HAT-RED-L", "quantity": 1, "price": 24.99},
        ],
    },
]

df = pd.json_normalize(
    orders,
    record_path="line_items",
    meta=["order_id", "customer_email"],
    sep=".",
)

df.to_csv("order_items.csv", index=False)

Output:

sku,quantity,price,order_id,customer_email
SHOE-BLK-42,1,89.99,5001,buyer@example.com
SOCK-GRY-M,3,7.99,5001,buyer@example.com
HAT-RED-L,1,24.99,5002,shopper@example.com

record_path tells pandas which array to expand. meta lists the parent fields to repeat on every row. Two orders with three total line items become three CSV rows.

When to use pandas

Use pandas when you're dealing with nested JSON, need to filter or transform data before export, or you're already in a data analysis workflow. It's the most powerful option and handles edge cases that would take dozens of lines with the standard library.

For a deeper dive into pandas-specific techniques — record_path, meta, and multi-level nesting — see our guide on converting nested JSON to CSV.

Method 3: Use an Online Converter

Not every JSON-to-CSV task needs code. If you have a file and just need a CSV, our JSON to CSV converter handles it in seconds:

  1. Paste your JSON or upload a file.
  2. The converter auto-detects nesting and flattens with dot notation.
  3. Download as CSV or Excel.

Everything runs in your browser — no data is sent to a server. This is the fastest path when you're dealing with a one-off file from a colleague or a quick API export that doesn't justify writing a script.

The converter handles the same edge cases described in this guide: nested objects, arrays of objects expanding to rows, primitive arrays joining inline, and mixed schemas where different records have different fields.

Handling Nested JSON

Nested JSON is the most common source of frustration. Here's a decision framework:

Nested objects (like customer.address.city): Use json_normalize in pandas or a recursive flatten function. Both produce dot-notation column names.

Arrays of primitives (like tags: ["billing", "auto"]): Join them into a single delimited string. Pipe (|) or semicolon (;) work better than commas since your output is already CSV.

Arrays of objects (like Shopify line items): Expand to multiple rows using record_path in pandas, or write a manual expansion loop.

Mixed nesting: Combine strategies. Flatten the nested objects, inline the primitive arrays, expand the object arrays.

For a complete walkthrough of every nesting scenario with working Python code, see our complete guide to converting nested JSON to CSV.

Handling Arrays of Objects

Arrays of objects are the most common structure in real API responses and the hardest to get right.

Consider a GitHub API response for repository contributors:

import json
import csv

repos = [
    {
        "repo": "tensorflow/tensorflow",
        "stars": 178000,
        "contributors": [
            {"login": "tensorflower-gardener", "contributions": 24891},
            {"login": "yifeif", "contributions": 2156},
        ],
    },
    {
        "repo": "pytorch/pytorch",
        "stars": 73000,
        "contributors": [
            {"login": "pytorchmergebot", "contributions": 18432},
        ],
    },
]

rows = []
for repo in repos:
    for contributor in repo.get("contributors", []):
        rows.append({
            "repo": repo["repo"],
            "stars": repo["stars"],
            "login": contributor["login"],
            "contributions": contributor["contributions"],
        })

with open("contributors.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=rows[0].keys())
    writer.writeheader()
    writer.writerows(rows)

Output:

repo,stars,login,contributions
tensorflow/tensorflow,178000,tensorflower-gardener,24891
tensorflow/tensorflow,178000,yifeif,2156
pytorch/pytorch,73000,pytorchmergebot,18432

Each contributor gets its own row, with the parent repo data repeated. This is the most useful structure for analysis — you can filter, sort, and pivot on any field.

Working with Real API Data

Toy examples use {"name": "Alice"}. Real APIs return this:

# Stripe Payment Intent (simplified)
{
    "id": "pi_3MtwBwLkdIwHu7ix28a3tqPD",
    "amount": 2000,
    "currency": "usd",
    "status": "succeeded",
    "customer": {
        "id": "cus_NffrFeUfNV2Hib",
        "email": "jenny.rosen@example.com",
        "name": "Jenny Rosen"
    },
    "payment_method_details": {
        "type": "card",
        "card": {
            "brand": "visa",
            "last4": "4242",
            "exp_month": 8,
            "exp_year": 2026
        }
    },
    "metadata": {
        "order_id": "6735"
    }
}

The real challenge isn't flattening — it's that different endpoints return different structures, fields are optional, and arrays can be empty. A production script needs to handle all of these:

import pandas as pd
import json

with open("stripe_payments.json") as f:
    payments = json.load(f)

df = pd.json_normalize(payments, sep=".", errors="ignore", max_level=3)

# Rename columns for cleaner CSV headers
df.columns = [col.replace("payment_method_details.", "pm.") for col in df.columns]

# Drop internal fields you don't need
df = df.drop(
    columns=[col for col in df.columns if col.startswith("metadata.")],
    errors="ignore"
)

# Handle any remaining nested objects (convert to JSON strings)
for col in df.columns:
    if df[col].apply(lambda x: isinstance(x, (dict, list))).any():
        df[col] = df[col].apply(
            lambda x: json.dumps(x) if isinstance(x, (dict, list)) else x
        )

df.to_csv("stripe_export.csv", index=False, encoding="utf-8")
print(f"Exported {len(df)} payments, {len(df.columns)} columns")

The errors="ignore" parameter prevents crashes when some payments are missing nested fields. The column renaming keeps your CSV readable. The final loop catches any objects that json_normalize didn't fully flatten and serializes them as JSON strings.

Which Method Should You Use?

| Scenario | Best method | Why | |---|---|---| | Flat JSON, quick script | json + csv | No dependencies, simple | | Nested API response | pandas.json_normalize | Handles nesting automatically | | One-off file conversion | Online converter | No code needed | | Lambda / Docker / CI | json + csv + manual flatten | No heavy dependencies | | Files > 1GB | ijson streaming | Won't exhaust RAM | | Data analysis pipeline | pandas end-to-end | Transform + export in one workflow |

Is your JSON flat? Use the standard library. Three lines, no dependencies, done.

Is your JSON nested but you just need a file? Use the online converter. Paste, click, download. No code.

Do you need to transform, filter, or analyze the data? Use pandas. json_normalize handles nesting, and once you're in a DataFrame you can do anything.

Are you in a constrained environment (Lambda, CI, minimal Docker)? Use the standard library with a recursive flatten function. Avoid the pandas dependency.

Are you dealing with files over 1GB? Use streaming with ijson or process in chunks with pandas chunksize. Standard json.load() will exhaust RAM on large files.

Common Errors and Fixes

[object Object] in CSV cells. Your script is calling str() on a nested object instead of flattening it. Use json_normalize or a recursive flatten function before writing to CSV.

UnicodeDecodeError when reading JSON. The file isn't UTF-8. Add encoding="utf-8-sig" (for files with a BOM) or detect the encoding with chardet:

import chardet

with open("data.json", "rb") as f:
    encoding = chardet.detect(f.read())["encoding"]

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

json.decoder.JSONDecodeError. The JSON is malformed — trailing commas, single quotes instead of double quotes, or unquoted keys. Validate it first with our JSON validator, or use the json5 library for lenient parsing.

Empty or extra rows in CSV. On Windows, always use newline="" in open() when writing CSV. Without it, csv.writer produces double line breaks.

KeyError on inconsistent records. Some records are missing fields that others have. Use DictWriter with restval="" and collect all keys with a union pass first (shown in Method 1 above).

Memory errors on large files. json.load() reads the entire file into RAM. Switch to streaming with ijson:

import ijson
import csv

with open("large_data.json", "rb") as f, \
     open("output.csv", "w", newline="", encoding="utf-8") as out:

    writer = None
    for record in ijson.items(f, "item"):
        if writer is None:
            writer = csv.DictWriter(out, fieldnames=record.keys())
            writer.writeheader()
        writer.writerow(record)

Frequently Asked Questions

What is the fastest way to convert JSON to CSV in Python?

For flat JSON: pd.read_json("data.json").to_csv("data.csv", index=False). Two lines. For nested JSON, add pd.json_normalize() between the read and write. For a no-code option, paste your JSON into the online converter.

Can I convert JSON to CSV without pandas?

Yes. Python's built-in json and csv modules handle flat JSON perfectly. For nested JSON without pandas, write a recursive flatten function and use csv.DictWriter to write the output. This keeps your script dependency-free.

How do I handle nested JSON when converting to CSV?

Use pd.json_normalize(data, sep=".") to flatten nested objects into dot-notation columns. For arrays of objects, use the record_path parameter to expand each element into its own row. For arrays of primitives, join them with a pipe delimiter. See the nested JSON to CSV guide for detailed examples of every scenario.

What if my JSON records have different fields?

Both pandas and csv.DictWriter handle this. In pandas, json_normalize with errors="ignore" fills missing fields with NaN. With the standard library, collect all keys across all records first, then pass them as fieldnames to DictWriter with restval="".

How do I convert a very large JSON file to CSV?

Don't use json.load() — it reads everything into RAM. Use the ijson library for streaming parse, or read in chunks with pandas chunksize. For files under ~500MB, pandas usually handles it fine. Above that, streaming is the safe choice.