Excel to JSON, CSV and SQL

Open an .xlsx in your browser and export it to JSON, CSV or SQL statements with CREATE TABLE. Detects dates and types, and the file never leaves your machine.

./excel-to-json

Drop your .xlsx file here, or click to choose it

It is not uploaded anywhere: it opens inside your browser.

The file is opened and converted in your browser. It is not uploaded to any server.

Compartir

What this tool does

It opens an .xlsx file inside your browser and converts it to three formats:

  • JSON — an array of objects using the first row as keys.
  • CSV — configurable delimiter, quoting per RFC 4180.
  • SQL — batched INSERT statements, with a CREATE TABLE inferred from your data, for MySQL, PostgreSQL or SQLite.

The file is not uploaded to any server. It is read, converted and downloaded without leaving your machine, which matters when the sheet holds payroll, supplier pricing or customer data.

How to use it

  1. Drop the .xlsx onto the upload area, or click to pick it.
  2. If it has several sheets, choose one.
  3. Tick whether the first row contains headers.
  4. Choose the output format, adjust the options and copy or download.

Dates: why almost every converter gets them wrong

If you have ever converted an Excel file and found a column of 45000, 45001, 45002 where you expected dates, this is why.

In an .xlsx file, dates do not exist as a data type. A date is stored as a number: days elapsed since a reference point. The 45000 above is 15 March 2023.

The only thing separating that number from any other quantity is the format applied to the cell, and that format lives in a separate file inside the .xlsx (xl/styles.xml). A converter that does not read it sees a number and copies a number. It is not broken: it is not looking in the right place.

This tool reads the style sheet, recognises date formats — both the ones Excel ships with and the custom ones you defined — and returns a real ISO date.

And the 1900 bug, dragged along for 40 years

Here is the detail almost no home-made implementation gets right.

Excel believes 1900 was a leap year. It was not: years divisible by 100 are not leap years unless they are also divisible by 400, and 1900 is not. The error comes from Lotus 1-2-3, which had it wrong in the eighties, and Microsoft copied it deliberately for compatibility: Microsoft documents it themselves. It has never been fixed because fixing it would shift every date in every spreadsheet in the world.

The consequence is concrete: serial number 60 is a 29 February 1900 that never existed, and that splits the conversion into two ranges:

SerialReal dateReference to use
11 January 190031/12/1899
5928 February 190031/12/1899
60does not exist
611 March 190030/12/1899
4500015 March 202330/12/1899

With a single reference you are right in one range and a whole day off in the other. And since real data almost always sits above serial 61, the bug never shows… until somebody enters an old date of birth and the conversion shifts it by a day.

Types are inferred from the data, not the header

Before generating anything, the tool walks each full column and assigns it a type, with a conservative rule: the type only applies if every non-empty value satisfies it. As soon as one value does not fit, the column falls back to text.

DetectsWhen
integerEvery value is a number without decimals
decimalEvery value is a number and at least one has decimals
booleanEvery value is true/false
dateEvery value is a date and every time is midnight
datetimeEvery value is a date and at least one carries a time
textAny other case

It also flags whether the column allows empties, and that is what decides the NOT NULL in the CREATE TABLE.

The type table is shown on screen before converting, so you can see at a glance whether a column is being read as text because it holds a dirty value: an “N/A” in a numeric column, a cell with a stray space, a hand-typed total at the bottom.

About the SQL it generates

It emits a CREATE TABLE with the inferred types and then the INSERTs grouped into batches (100 rows per statement by default, configurable). The three engines are treated as what they are:

MySQL / MariaDBPostgreSQLSQLite
Identifiers`col`"col""col"
BooleanTINYINT(1), 1/0BOOLEAN, TRUE/FALSEINTEGER, 1/0
Date and timeDATETIMETIMESTAMPDATETIME
DecimalDECIMAL(15,4)DECIMAL(15,4)REAL

Escaping. Values are always escaped by doubling the single quote (O'Brien'O''Brien'), which is what the SQL standard says and what all three engines understand identically. Backslashes are deliberately not used: in MySQL they work, but their behaviour depends on the NO_BACKSLASH_ESCAPES mode, and escaping that depends on server configuration is not escaping.

Even so, review the CREATE TABLE before running it. The types and lengths are a reasonable proposal derived from the sample you gave it, not a schema design: it does not know which field is the primary key, which one needs an index, or whether that text column is going to grow.

Headers get sanitised too

An Excel header can be Fecha de alta, Precio ($) or 2026 total, and none of the three works as a column name. Next to the original name you will see the generated identifier:

  • Fecha de altafecha_de_alta
  • Añoano (no accents, no ñ)
  • Precio ($)precio
  • 2026 totalcol_2026_total (no engine accepts an identifier starting with a digit)
  • An empty header → col_3, from its position

And if two headers produce the same identifier — id, ID and Id are the same thing once sanitised — a numeric suffix is added so the CREATE TABLE does not fail.

Which formats it accepts

Only .xlsx, the format from Office 2007 onwards. Files exported by Google Sheets, LibreOffice Calc and Numbers also work, since they follow the same standard.

What it does not accept:

  • .xls (Office 2003 and earlier). It is an entirely different binary, not a ZIP of XML. Open it in Excel or LibreOffice and save it as .xlsx.
  • .csv. It is already plain text; the JSON and CSV converter covers that.
  • .ods from LibreOffice. Save it as .xlsx.

No external dependencies, and why that matters here

An .xlsx is a ZIP full of XML. Reading it takes two things the browser already ships: DecompressionStream to decompress and an XML parser.

It is worth saying why the usual library for this is not used. The xlsx package on npm is frozen at version 0.18.5 from 2022 and carries two known vulnerabilities — prototype pollution (CVE-2023-30533) and a regular-expression denial of service (CVE-2024-22363). Its authors moved distribution to their own CDN and never published the fixes to npm, so installing xlsx from there today means installing the vulnerabilities. Writing the reader, by contrast, is around three hundred lines and adds no attack surface.

Frequently asked questions

Is my Excel file uploaded to a server?

No. The .xlsx is opened and converted inside your browser with JavaScript: it is not uploaded, not stored and not logged. That is the difference that matters when the sheet holds payroll, supplier pricing, customer data or anything that should not pass through someone else's server. You can verify it by disconnecting the network after the page loads: conversion keeps working.

Why do my dates come out as numbers like 45000?

Because in an .xlsx dates do not exist as a type: they are numbers counting the days elapsed since a reference date, and the only thing separating them from any other quantity is the format applied to the cell. A converter that does not read the style sheet sees the number and copies it verbatim. This tool does read the styles, recognises date formats — both built-in and custom — and returns a real ISO date. If you still get a number, that cell had no date format in the source Excel.

What is the year 1900 bug and how does it affect my dates?

Excel treats 1900 as a leap year, for compatibility with Lotus 1-2-3 back in the eighties. It was not: serial number 60 corresponds to a 29 February 1900 that never existed. The consequence is that converting a serial number to a date needs two different reference points, one for values before that phantom day and another from it onwards. Using a single one is right in one range and one day off in the other, and since real data almost always falls in the good range, the error goes unnoticed until an old date shows up.

How does it decide each column's data type?

By looking at the values, not the header. It walks the whole column and only assigns a type if every non-empty value satisfies it: integers give integer, any decimal gives decimal, all dates give date — distinguishing date from datetime depending on whether every time is midnight — and as soon as something does not fit it falls back to text. It also flags whether the column allows empties, which is what decides the NOT NULL in the CREATE TABLE.

Is the generated SQL safe to paste directly?

Values are always escaped: single quotes are doubled, which is what the standard says and what MySQL, PostgreSQL and SQLite all understand. Backslashes are not used, because in MySQL their behaviour depends on the NO_BACKSLASH_ESCAPES mode and that makes them unreliable. Even so, always review the CREATE TABLE before running it: the types and lengths are a reasonable proposal derived from your data, not a schema design.

Does it support old .xls files or .csv?

Only .xlsx, the format since Office 2007. The old .xls is an entirely different binary, and .csv is already plain text — the JSON and CSV converter covers that. If you have an .xls, open it in Excel or LibreOffice and save it as .xlsx. Files exported by Google Sheets, LibreOffice and Numbers also work.

Reviews & ratings

No reviews yet. Be the first to leave one!

Write a review

Your rating *