Calci.inCalculate Today for a Better Tomorrow

JSON to Excel Converter

Nested JSON flattened into a spreadsheet

The guide

Flattening JSON into rows and columns

How nested JSON maps onto a spreadsheet, which list becomes the main sheet, what Excel's limits cut, and why the .xlsx keeps leading zeros that Excel can drop from a CSV.

Last reviewed · 1,804 words

In short

  • Download the .xlsx rather than the CSV when the data holds codes with leading zeros or whole numbers longer than 15 digits. A CSV has no way to mark a cell as text.
  • To see which rows of a child sheet belong to which parent, filter on its link column, such as "data #". It holds the parent's number from the # column.
  • Tick the option to copy the parent's columns onto child rows when each sheet has to sort and filter on its own.
  • The main sheet is the longest list that is not inside another list, and a tie goes to the one found first. Every other list is on its own sheet in the same file.
  • When the JSON will not read, go to the line and column shown. A trailing comma is marked at the comma itself.
  • A .jsonl or .ndjson file with two or more lines needs no editing first. Each line becomes a row.

You have JSON from an API, an app export or a developer, and someone needs it in Excel. JSON is a tree and a worksheet is a grid, so every converter has to decide what counts as a row, what a nested object becomes and where a list goes. These are the rules the converter above follows, the Excel limits they meet, and how to find the mistake when the JSON will not read.

Nested objects become dotted columns

Take two orders, each with a customer object, a list of tags and a list of items:

[
  {
    "id": 101,
    "customer": { "name": "Asha Rao", "city": "Pune", "pin": "411001" },
    "tags": ["new", "priority"],
    "items": [
      { "sku": "TSHIRT-M", "qty": 2 },
      { "sku": "MUG-01", "qty": 1 }
    ]
  },
  {
    "id": 102,
    "customer": { "name": "Rahul Mehta", "city": "Jaipur", "pin": "302001" },
    "tags": [],
    "items": [{ "sku": "SHOES-9", "qty": 1 }]
  }
]

A nested object gets no sheet of its own: each field inside it becomes a column named by its path, such as customer.city. The main sheet from that JSON is:

#idcustomer.namecustomer.citycustomer.pintags
1101Asha RaoPune411001new; priority
2102Rahul MehtaJaipur302001

Columns keep the order they are first met in, so a field that only a later record has goes after the columns already seen. A key containing a dot can collide with a nested one: {"a.b": 1} and {"a": {"b": 2}} both print as a.b, share one column, and the first value in the row is kept.

Lists of plain values share one cell

A list of text or numbers, such as tags, is not worth a sheet. It is joined into one cell with a semicolon and a space, so ["new", "priority"] becomes new; priority.

In the JSONIn the spreadsheet
["new", "priority"]new; priority
["a", null, "b"]a; null; b
[]a blank cell
[1, {"b": 2}], objects mixed with plain valuesthe list as JSON text
[[1, 2], [3]], lists inside a listthe list as JSON text

The last two have no fair grid layout, so they go in as JSON text and the page counts them. A key whose list or object is empty in every record still gets its column, so you can see the field exists.

Lists of objects become linked sheets

Items are different: each has fields of its own, and an order can have any number. Every item from every order goes on one sheet named after its path, and a number column ties each item to its order. The main sheet gains a # column numbering its rows, and the items sheet a data # column holding the parent's number:

data #skuqty
1TSHIRT-M2
1MUG-011
2SHOES-91

The link column takes the parent's name followed by #. A file shaped {"customers": [{"name": …, "orders": [{"no": …, "items": [...]}]}]} gives three sheets, customers, customers.orders and customers.orders.items, linked by customers # and customers.orders #. The middle sheet has both a # column of its own and a link column. If the same customers are a plain list instead, the sheets are data, orders and orders.items. To see one order's items, filter the child sheet on its link column; the .xlsx has a filter on each header row.

Copying parent columns down

With only a link column, answering "who bought this?" needs a lookup. The option to copy the parent row's columns repeats every ancestor field on each child row, named <parent>.<column>. For a child of a top-level sheet that has no lists of its own:

child columns = 1 link column + the parent's own columns + the child's own columns

The items sheet in the orders example then has 1 + 5 + 2 = 8 columns: data #, data.id, data.customer.name, data.customer.city, data.customer.pin, data.tags, sku and qty. The parent's # is not copied. Further down the copies accumulate, so in the customers file each item row carries customers.name and customers.orders.no as well as its own fields.

Which list becomes the main sheet

When the JSON is a list, each item is a row on a sheet called data. An object may hold several lists, so it needs a rule. Every list of objects reached through objects alone becomes a sheet, and the one with the most rows comes first as the main sheet. A tie goes to the list found first. A list inside another list's rows is a child sheet, and never the main one. The object's loose fields go on a one-row sheet called details, placed last.

{
  "shop": "Asha Stores",
  "city": "Pune",
  "orders": [{ "no": 1 }, { "no": 2 }, { "no": 3 }],
  "returns": [{ "no": 2 }]
}
SheetRowsWhy
orders3the longest list, so first
returns1another list of objects
details1shop and city, outside any list

The main sheet is the one the preview opens on and the CSV holds unless you switch. Shared wrappers are trimmed from names: in {"response": {"data": {"orders": [...], "returns": [...]}}} the common response.data is dropped, leaving orders and returns. An object whose values are all records with largely the same fields, such as {"u1": {...}, "u2": {...}}, is read as rows, with the names in a first column called key, or _key if a record already has a field called key.

Leading zeros and numbers past 15 digits

Leading zeros survive only as text. JSON does not allow a number to be written with leading zeros, and the converter reports 007 as a mistake, so a code such as 007 has to arrive quoted, as "007". The .xlsx writes quoted values as text cells, so "411001" and "07" stay as written, while real numbers become number cells you can add up. JSON's true and false become boolean cells, and null an empty cell.

Excel stores only 15 significant digits of a number, so a longer one such as 12345678901234567890 cannot be held exactly: the digits after the fifteenth are lost.

There is an earlier limit. JavaScript, which reads the JSON in your browser, guarantees to hold whole numbers exactly only up to 2^53 − 1 = 9007199254740991, its Number.MAX_SAFE_INTEGER. Past that, a plain parse turns 9007199254740993 into 9007199254740992 before Excel ever sees it.

The converter reads each number's original digits and writes any whole number longer than 15 digits as text. That needs a browser whose JSON.parse passes each number's source text to a reviver: Chrome and Edge from version 114, and recent Firefox and Safari. Elsewhere a warning says such numbers may be rounded. Decimals are not covered.

CSV or XLSX

A CSV has no way to mark a cell as text or as a number, so a spreadsheet opening one has to guess. The CSV of the orders begins:

#,id,customer.name,customer.city,customer.pin,tags
1,101,Asha Rao,Pune,411001,new; priority

The file is exact. What changes the data is Excel's guess on opening: 07 can come back as 7, and a 20-digit number loses every digit after its fifteenth.

.xlsx.csv
Sheetsall, in one filethe sheet being previewed
Codes such as 007text, zeros keptExcel may drop the zeros
Whole numbers over 15 digitstext, every digit kept in a supporting browserExcel may round them
true and falseboolean cellsthe words
Header filteryesno
Columns past 16,384cutkept
Text past 32,767 characterscutkept

The CSV is UTF-8 with a byte-order mark, which lets Excel show ₹ and accented letters, and has CRLF line ends. Use the .xlsx when a person opens the file in Excel, and the CSV when a program reads one flat table. For scale, 14.3 MB of JSON for 30,000 customers gave a 9.07 MB .xlsx holding all three sheets, of 30,000, 60,000 and 60,000 rows, and a 3.29 MB CSV of the main sheet alone.

Excel size limits and sheet names

An Excel worksheet holds at most 1,048,576 rows by 16,384 columns, and 32,767 characters in a cell. The header takes a row, so:

data rows per sheet = 1,048,576 − 1 = 1,048,575

Rows past that are left out of both downloads, and the page counts them. A child sheet collects the rows of every parent, so it can reach the limit while its parent is far short.

Sheet names allow at most 31 characters and none of \ / ? * [ ] :. Long paths are shortened from the left, so the part that says what the sheet holds survives:

PathSheet name
customers.orders.items.variants.prices.history (46 characters)items.variants.prices.history
a/ba_b
HistoryHistory_
items, then Itemsitems, Items (2)

A sheet named like 2024 or A1 gets no header filter, which would otherwise make Excel offer to repair the file.

JSON Lines files

JSON Lines, saved as .jsonl or .ndjson, has one complete JSON value per line, no brackets round the file and no commas between lines. The converter splits the text at each line break and trims every line, so Windows line ends work too and blank lines are skipped.

Paste or open such a file as it is: each line becomes a row, and a note says it was read as JSON Lines. There must be at least two lines, each valid on its own. If one is broken, the text is treated as a single document, so the error position may not be the broken line; paste suspect lines alone to find it. Two objects on one line are not JSON Lines: wrap them in [ ] with a comma between, or give each its own line.

Finding the mistake in broken JSON

The converter reports the first mistake by line and column, shows that line with a caret under the fault, and names the likely cause in plain English. Its own scanner writes the message, so it reads the same in every browser.

{
  "name": "Asha",
  "tags": ["new", "priority",]
}

This reports line 3, column 29: a comma before the ]. Column 29 is the comma, not the bracket, so the character to delete sits above the caret.

What was writtenWhat JSON wants
'Asha'"Asha"
{name: "Asha"}{"name": "Asha"}
// note or /* note */nothing: JSON has no comments
True, False, Nonetrue, false, null
undefinednull
NaN, Infinitynull, or the word in quotes as text
007"007"
"C:\Users""C:\\Users"
“curly quotes”straight "

Invisible no-break and zero-width spaces, often pasted from a chat or web page, are named too. To check or tidy JSON without a spreadsheet, use the JSON Formatter & Validator.

What this tool assumes

  • It reads structure, not meaning: names come from the JSON's own keys. GST portal return files have their own GSTR JSON to Excel Converter.
  • Up to 20 MB (20 × 1,024 × 1,024 bytes) per conversion. A file over 1 MB is converted straight from the file instead of opening in the box.
  • Everything runs in your browser tab. At desktop speed a 14 MB file took about 0.5 s to read and flatten and about 2.2 s to write the .xlsx; slower phones take longer.
  • The .xlsx is compressed less tightly than Excel's own saves, so it is roughly three times the size Excel would make.
  • The preview shows the first 50 rows and 60 columns of each sheet. The downloads have every row and column, within the Excel limits above.
  • Nesting deeper than 50 levels is written into the cell as JSON text.
  • Nothing is uploaded. The spreadsheet library is loaded from calci.in the first time you download an .xlsx, without your data.

Sources

Frequently asked questions

Is my JSON uploaded anywhere?

No. The JSON is read in your browser tab, and the Excel and CSV files are made on your device. The spreadsheet library is loaded from calci.in the first time you download an Excel file, and nothing with your data in it is sent. Close the tab and it is gone.

How are nested objects and arrays turned into columns?

A nested object becomes dotted columns: {"address": {"city": "Pune"}} fills a column named address.city. A list of plain values, such as tags, is joined into one cell with "; " between the items. A list of objects inside each row, such as the items of an order, goes on its own sheet. Each item row has a column like "data #" that gives its order's row number, which matches the # column on the order sheet. Tick the option to copy the order's columns onto every item row too.

Which part of my JSON becomes the rows?

If the JSON is a list, each item is a row. If it is an object, every list of objects inside it becomes a sheet, and the one with the most rows is the main sheet: it comes first and is the one the CSV is made from. Fields outside any list go on a sheet called details, at the end. An object whose values are all records with largely the same fields, such as {"u1": {...}, "u2": {...}}, is read as rows, with the names in a key column.

Why does it say my JSON has a mistake?

The converter shows the line and column of the first mistake, with the line itself and a pointer under it. The usual causes are a comma after the last item, single quotes instead of double quotes, names without quotes, comments, and Python's True, False or None in place of true, false and null. A file with one JSON object on each line (JSON Lines) is read as rows without any change.

Will Excel keep leading zeros and long numbers?

In the .xlsx file, yes. Text such as "007" or a PIN code in quotes stays text, and a whole number longer than 15 digits, such as an account number, is kept as text because Excel holds only 15 digits of a number and would round the rest. A CSV has no way to say which cells are text, so Excel may drop leading zeros when it opens one. Use the .xlsx when that matters.

How much JSON can it convert?

Up to 20 MB at a time. The preview shows the first 50 rows of each sheet, and the downloads have every row. An Excel sheet holds 1,048,576 rows, 16,384 columns and 32,767 characters in a cell, and the page tells you when your data goes past any of these. The CSV keeps the extra columns and the full text; rows past the limit are left out of both files.