Skip to the tool workspace
Browser 123

Handleidingen Data and documents

How to turn JSON into a spreadsheet someone can read

Get an API response or a JSON export into Excel: the jq command for flat records, what to do with nested objects and arrays, and why long IDs need to travel as text.

JSON is easy for programs to read and awkward for people. A spreadsheet wants rows and columns, and an API response is a list of records that often has records inside records. If every record is flat, one jq command makes the CSV. If they are nested, you decide which fields become columns.

The JSON to XLSX Converter after a run: customers.json converted, with customers.xlsx, its size and a Download button

A JSON file converted in the browser, nested fields as their own columns.

The direct wayjqmacOS · Linux · Windows

jq comes with recent macOS (/usr/bin/jq) and is one install away elsewhere (sudo apt install jq, winget install jqlang.jq). For an array of flat records:

jq -r '(map(keys_unsorted) | add | reduce .[] as $c ([]; if index([$c]) then . else . + [$c] end)) as $k
  | $k, (.[] | [.[$k[]]]) | @csv' records.json > records.csv

The first line collects every key that appears in any record, in the order it first appears. The second writes that header and then one row per record.

The example file

Every command here was run on this file, a short customer list of the kind a REST API returns. It has a quote and a line break inside values, a null, a nested address, a tags array, and a phone that only the last record has.

[
  {
    "id": 10482,
    "name": "Ana Lima",
    "email": "[email protected]",
    "postcode": "01234",
    "signed_up": "2026-03-14",
    "active": true,
    "note": "Prefers email, not phone",
    "address": { "street": "Rua Augusta 12", "city": "Lisbon", "country": "PT" },
    "tags": ["vip", "newsletter"]
  },
  {
    "id": 10483,
    "name": "Ben \"Benny\" Okafor",
    "note": "Two lines:\nask for the invoice",
    "address": { "street": "4 Main St", "city": "Cambridge", "country": "US" },
    "tags": []
  },
  {
    "id": 10484,
    "name": "Chloé Martin",
    "note": null,
    "address": { "street": "8 rue Oberkampf", "city": "Paris", "country": "FR" },
    "tags": ["trial"],
    "phone": "+33 6 12 34 56 78"
  }
]

(The second and third records also have email, postcode, signed_up and active; they are left out here to save space.)

Flat records: one command

The command you usually see online takes the column names from the first record:

jq -r '(.[0] | keys_unsorted) as $k | $k, (.[] | [.[$k[]]]) | @csv' records.json

That works until one record has a field the first one doesn't. With address and tags removed from the example, it writes every row and quietly drops phone, because Ana has no phone. The command in the box above reads the keys from every record, so the header gets phone and the rows without one get an empty cell:

"id","name","email","postcode","signed_up","active","note","phone"
10482,"Ana Lima","[email protected]","01234","2026-03-14",true,"Prefers email, not phone",
10483,"Ben ""Benny"" Okafor","[email protected]","02139","2026-05-02",false,"Two lines:
ask for the invoice",
10484,"Chloé Martin","[email protected]","75011","2026-07-21",true,,"+33 6 12 34 56 78"

@csv does the quoting properly: the quote in Ben's name is doubled, the line break stays inside a quoted cell, and null becomes an empty cell. Read back with Python's csv module, that is four rows, with Ben's note as one value on two lines.

To open the CSV in Excel without losing the zero in 01234 or getting the dates turned round, see how to convert CSV to Excel without breaking the data.

Nested records: choose the columns

Run the first-record command on the full example and jq writes the header, then stops at the first nested value (the command in the box stops the same way):

"id","name","email","postcode","signed_up","active","note","address","tags"
jq: error (at customers.json:36): object ({"street":"...) is not valid in a csv row

A CSV cell holds one value, so an object or an array has to become something else first. For a spreadsheet a person will read, the best answer is usually to pick the fields that matter. A path like .address.city reaches into the nested object, and join turns an array into one cell:

jq -r '["id","name","email","city","country","tags"],
  (.[] | [.id, .name, .email, .address.city, .address.country, (.tags | join(", "))])
  | @csv' customers.json > customers.csv
"id","name","email","city","country","tags"
10482,"Ana Lima","[email protected]","Lisbon","PT","vip, newsletter"
10483,"Ben ""Benny"" Okafor","[email protected]","Cambridge","US",""
10484,"Chloé Martin","[email protected]","Paris","FR","trial"

Ben's empty tags array becomes an empty cell. Six named columns beat a dozen generated ones when the file is going to someone in sales.

Every field, straight to Excel: pandas

If you want every field as a column and an .xlsx at the end, Python's pandas does both. json_normalize turns address into address.street, address.city and address.country. It leaves arrays alone, so join them yourself:

import json, pandas as pd

rows = json.load(open("customers.json", encoding="utf-8"))
df = pd.json_normalize(rows)              # address -> address.street, address.city, ...
df["tags"] = df["tags"].str.join(", ")    # ["vip", "newsletter"] -> "vip, newsletter"
df["id"] = df["id"].astype(str)           # keep IDs as text
df.to_excel("customers.xlsx", index=False)

It needs pip install pandas openpyxl. We opened the result with openpyxl to check it: twelve columns (id, name, email, postcode, signed_up, active, note, tags, address.street, address.city, address.country, phone), the postcodes stored as text with their zeros, active as real TRUE/FALSE values, and Ben's note on two lines in one cell.

Leave out the tags line and it still runs, but the cells hold Python's spelling of the list, ['vip', 'newsletter'], and Ben's says [].

Miller does it in one line, with two quirks

mlr --ijson --ocsv flatten then unsparsify customers.json flattens everything with no script (Miller 6.13). Arrays become numbered columns (tags.1, tags.2), an empty array becomes a separate tags column holding [], and null is written as the word null. Leave out unsparsify and it stops at the first record with different fields: mlr: CSV schema change.

Long numbers and IDs

Excel keeps 15 significant digits. A 16-digit order number or a database ID past 9007199254740991 doesn't survive being a number, and JavaScript, which every browser tool uses to read JSON, has the same limit. Given {"id": 9007199254740993}, JSON to XLSX and pandas both wrote 9007199254740992, one less than the real ID, and so does JSON to CSV. jq kept it, and (.id | tostring) in a jq command writes it as text, so Excel keeps it too. Below that limit the browser tools keep every digit: a 13-digit barcode 4006381333931 and a millisecond timestamp 1760745600000 come out of JSON to CSV as written.

If the IDs are yours to change, make them strings in the JSON. If they come from an API, convert them to text in the script (the astype(str) line above) before anyone opens the file.

In the browser

The two converters read the JSON the same way: each record is a row, each key is a column, and keys that only some records have still get a column, with empty cells where they are missing. Nested objects become dotted columns (address.street, address.city, address.country), and a list goes into one cell as its JSON text. Numbers stay numbers, true and false become TRUE and FALSE, and null is an empty cell. An array of arrays works too: its first row is the header.

The example file, through JSON to CSV:

id,name,email,postcode,signed_up,active,note,address.street,address.city,address.country,tags,phone
10482,Ana Lima,[email protected],01234,2026-03-14,TRUE,"Prefers email, not phone",Rua Augusta 12,Lisbon,PT,"[""vip"",""newsletter""]",
10483,"Ben ""Benny"" Okafor",[email protected],02139,2026-05-02,FALSE,"Two lines:
ask for the invoice",4 Main St,Cambridge,US,[],
10484,Chloé Martin,[email protected],75011,2026-07-21,TRUE,,8 rue Oberkampf,Paris,FR,"[""trial""]",+33 6 12 34 56 78

Twelve columns, the same twelve pandas makes, in the order the keys first appear. The lists read ["vip","newsletter"] rather than vip, newsletter: if that matters, join them with jq or pandas instead.

  1. Open JSON to XLSX (or JSON to CSV) and drop your .json on the page, or choose it. The file must be an array: of records, [{...}, {...}], or of rows, [[...], [...]].
  2. Click Convert 1 File.
  3. Under Converted Files, click Download.
The JSON to CSV Converter after a run: customers.json converted, with customers.csv, its size and a Download button

The same file as a CSV.

Which to use

Your JSON Best route Why
Flat records, you have a terminal jq (box above) One command, exact numbers, proper quoting
No terminal, or a phone JSON to XLSX Nothing to install; nested fields get columns; text stays text
Nested, for a person to read jq with named fields You choose the columns, nothing else
Nested, every field as a column JSON to XLSX, or pandas json_normalize Both give address.city its own column; pandas lets you join lists your way
IDs longer than 15 digits jq with tostring, or pandas astype(str) Keeps them as text

Excel can also open JSON directly, through Power Query (Get Data), on Windows and in Microsoft 365 for Mac: see Microsoft's Import and shape data in Excel for Mac. Nested records show as Record and List cells that you expand column by column. That suits a file you will refresh every week more than one you open once.

When you have the file, open it once yourself before sending it on: check that a long ID or a postcode in the first rows looks the way it does in the JSON.

Questions people ask

My JSON starts with { and not [. Will it convert?

Only if the records are the whole file. Most API responses wrap the list in an object, such as {"data": [...]} or {"results": [...]}. Pull the list out first: jq '.data' response.json > records.json. The browser tools turn a single object into one row, which is rarely what you want.

Why did my ID column turn into 1.23457E+11?

Something on the way treated the ID as a number and wrote it in a short display form. Excel shows long numbers that way when it opens a CSV, even when the file has every digit, as our converters write them. Make IDs strings in the JSON, or open the CSV with the column set to Text. IDs longer than 15 digits must be strings, because Excel cannot hold more than 15 significant digits.

How do I flatten nested JSON for Excel?

Give each nested field its own column (address.city, address.country) and join arrays into one cell (vip, newsletter). In Python, pandas.json_normalize does the first part, and one line joins the arrays. With jq, list the fields you want: .address.city reaches into the nested object. Our JSON to XLSX and JSON to CSV make the dotted columns themselves and put each array in one cell as its JSON text.

Should I make a CSV or an XLSX?

An XLSX, if a person will open it in Excel. It keeps text as text, so a postcode like 01234 keeps its zero. A CSV is the right choice when another program will import it, because it is plain text and every database reads it.

Tools in deze handleiding

Alles draait op je apparaat

Alle handleidingen First published 18 okt. 2026

/
123 items
Draait in je browser·Je bestanden blijven op dit apparaat
Out0 B
Ready—