Turn an old Excel workbook into JSON
An .xls file is the workbook format Excel used until 2007, and files in that format still turn up: an export from an older accounting or stock system, or a form that only accepts .xls. Convert XLS to JSON and that data can go into a web app or a script, which read JSON directly but can't open a binary spreadsheet. This page reads your XLS workbook and writes a JSON file with one record per row, using the column headings as the keys.
This page is built for the old Excel to JSON case. If your file ends in .xlsx, use XLSX to JSON instead, which follows the same rules but is written for the newer format.
How to convert XLS to JSON
- Upload your XLS file - Drag it into the box at the top of this page, or browse for it. You can add up to 5 files at once, and each one can be up to 20 MB.
- Check the output - JSON is already picked on this page, so there's nothing to change.
- Download your file - Save the .json file when it's ready. In the backend team's production runs, small files took 11 to 18 seconds from upload to download.
The conversion runs on our servers, not in your browser, so there's nothing to install and no account to make. The download link works for 30 minutes. Our security page explains what happens to the file after that.
How the converter reads an .xls file
XLS is a binary format from the 1990s, and the converter doesn't read it directly. LibreOffice opens the file on our server and saves it again as .xlsx, and that copy is read cell by cell. Two things follow from that: the JSON comes out exactly as it would from an .xlsx, and a file that is really an HTML table or an Excel 2003 XML spreadsheet saved as .xls converts too, because LibreOffice opens those as well.
From the cells, the JSON is built like this:
- If one sheet holds data, the output is a plain list of records. If more than one does, it's an object keyed by sheet name, such as
{"Orders": [...], "Customers": [...]}. Hidden sheets are included, and empty sheets are left out. - The first row that isn't empty is the header row, and its cells become the keys. Headings are trimmed, a blank heading becomes
column_3(its position), a repeated heading gets_2added, and a number used as a heading becomes text, like"5". A column with no heading and no values is dropped. - Each row below the header is one record, with the keys in column order. An empty cell gives
"", nevernull, and a row with nothing in it is skipped. - Number cells stay JSON numbers. They're rounded to the 15 digits Excel keeps, so a cell holding 0.1+0.2 gives
0.3, and whole numbers up to 15 digits come out exactly. - Text cells stay text, so a ZIP code stored as text keeps its leading zero:
"01100". - TRUE and FALSE cells become
trueandfalse. - A cell formatted as a date becomes ISO text rather than Excel's serial number. A date at midnight gives
"2024-01-15", a date with a time gives"2024-01-15T09:30:00", and a time on its own gives"14:05:00". - A formula cell gives its result, never the formula text. If the file was saved without results, LibreOffice works them out while it rewrites the file.
- An error cell such as
#N/Acomes out as that text. In a merged range, the top-left cell has the value and the others give"". Hidden rows are included.
The file is written with 2-space indentation, as UTF-8 without a byte order mark. Accented and non-Latin text is written as it is, not as \u escape codes.
A real example from our tests
On 13 November 2026 we uploaded a 6,144-byte .xls file to our live converter. It's the workbook our JSON to XLS page had made earlier that day from a two-record JSON file, so we knew what was in it: one sheet named Data, holding these cells.
| id | name | address.city | tags | zip |
|---|---|---|---|---|
| 1 | Ana | Lisbon | a; b | 01100 |
| 2 | Zoë | Zürich | 8001 |
About 13 seconds after the upload started, this came back:
[
{
"id": 1,
"name": "Ana",
"address.city": "Lisbon",
"tags": "a; b",
"zip": "01100"
},
{
"id": 2,
"name": "Zoë",
"address.city": "Zürich",
"tags": "",
"zip": "8001"
}
]
The id cells were numbers in the sheet and are numbers here, 1 and 2 without quotes. Both ZIP codes were text cells, so "01100" kept its zero and 8001 is in quotes too. The empty tags cell became "", and Zoë and Zürich kept their accents. Notice that address.city is a flat key. The original JSON had a city nested inside an address record, which JSON to XLS flattened into a dotted heading, and this converter doesn't rebuild the nesting from the dot. You can download the XLS we uploaded and the JSON it produced to check them yourself.
The backend team ran a second .xls on production the same day, with number, text and TRUE/FALSE columns:
| id | name | zip | balance | active | |
|---|---|---|---|---|---|
| 1 | Ana Silva | ana@example.com | 01100 | 1520.75 | TRUE |
| 2 | Ben Okafor | ben@example.com | 10001 | 0 | FALSE |
| 3 | Chen Wei | 02134 | -42.5 | TRUE |
[
{
"id": 1,
"name": "Ana Silva",
"email": "ana@example.com",
"zip": "01100",
"balance": 1520.75,
"active": true
},
{
"id": 2,
"name": "Ben Okafor",
"email": "ben@example.com",
"zip": "10001",
"balance": 0,
"active": false
},
{
"id": 3,
"name": "Chen Wei",
"email": "",
"zip": "02134",
"balance": -42.5,
"active": true
}
]
The balance column came through as 1520.75, 0 and -42.5, all numbers. The TRUE and FALSE cells in the active column became true and false, and Chen Wei's empty email cell became "".
Before launch, on our test server, a .xls with 65,000 rows converted in 68 seconds. An .xls sheet holds at most 65,536 rows, so that is close to a full sheet, and well inside the 960-second time limit.
Things to check before you upload
- Only real TRUE/FALSE cells become JSON booleans. If someone typed the word true into a text cell, the JSON gets
"true"in quotes, which a script will treat as text. - Dotted headings don't become nested records. A column called
address.citystays a key calledaddress.city, so if you need{"address": {"city": ...}}, you'll have to rebuild that yourself. - Error cells like
#N/Aand#DIV/0!arrive as text, not asnull, so a script that expects a number in that column needs to handle those strings. - Excel keeps 15 significant digits for a number, so a 16-digit ID stored as a number has already lost its last digit before the file reaches us. Store long IDs as text in the sheet. A very large number like 1e16 is written as
1e+16.
Limits, and files that won't convert
- Each file can be up to 20 MB. The upload box accepts bigger files, but the converter stops anything over that with the message
This file is larger than 20 MB. Try splitting it into smaller files. - A 0-byte file gives
This file is empty.A workbook with sheets but no data in any of them givesThis spreadsheet has no data to convert. - A damaged file, or a file that isn't really a spreadsheet, gives
We couldn't read this spreadsheet. The file may be damaged or password-protected.The backend team confirmed this on production by uploading a file of random bytes named .xls. - A workbook can hold at most 5,000,000 filled cells across all its sheets, and the reader stops at 50,000,000 cells including empty ones. One .xls sheet can hold 65,536 rows by 256 columns, which is 16.7 million cells, so a few packed sheets can hit these limits. The messages ask you to split the file.
- LibreOffice gets 180 seconds to rewrite the file and the reader gets 90 seconds of processing time after that. Past either, you see
This spreadsheet took too long to read. Try splitting it into smaller files.The whole conversion stops at 960 seconds, and a JSON file that would be larger than 200 MB is refused.