How to convert CSV to JSON

Five ways to do it, what each one costs you, and the four things about the CSV format that turn a two-line conversion into an afternoon.

Free toolCSV to JSON ConverterConvert either way, with the delimiter detected, a preview table, and quoted fields handled properly.Open the converter

The short answer

Use a real CSV parser, and decide what you want to happen to types. Those are the only two decisions. Everything else is a matter of which language you are already in.

A one-off conversion belongs in a browser tool. A conversion you will run again belongs in a script, because next month's file will have a new column in it. A file too big to hold in memory belongs in a streaming tool writing NDJSON. What none of them should be is a split on commas, for the reason in the next section.

Four things about CSV that break a conversion

1. Quoting

A field may contain the delimiter, as long as it is quoted. So this file has two columns, not three:

name,note
Ada,"Keyboard, 88 key"

A quoted field may also contain a doubled quote standing for one character, and a line break, which means one record can span several lines of the file. Any conversion that assumes one line is one record will read that file wrongly and not tell you. This is the whole reason to use a parser rather than split.

2. The delimiter is not always a comma

Excel writes a semicolon on any machine whose locale uses a comma as the decimal separator, which is most of continental Europe. Databases and log tools often write tabs or pipes. A good converter detects it; a good script takes it as an argument rather than hard-coding a comma and failing silently on the first file from the Berlin office.

3. Encoding

CSV carries no encoding declaration. A file is bytes, and the reader guesses. UTF-8 read as Windows-1252 turns é into é; the fix is a byte order mark at the front, which Excel treats as proof of UTF-8. In Python, read those files with encoding='utf-8-sig' so the mark is stripped rather than becoming part of your first column name.

4. Excel has already changed your data

This is the one people find last. If the CSV was opened and saved in Excel on its way to you, leading zeros are gone, anything date-shaped has been converted, and long numbers are in scientific notation. No converter can undo that, because the information is not in the file any more. Get the original export, and open it in a text editor to check before you convert.

Five ways to do it

MethodGood forWatch out for
Browser toolA one-off file, and anything confidential.Holds the file in memory, so there is a size ceiling.
Python, stdlibA repeatable job with no dependencies.Every value arrives as a string. That is a feature.
pandasNumbers you are about to analyse.Guesses types, and will wreck an ID column unless told not to.
Miller or jqPipelines, and files bigger than memory.Another tool to install, and jq cannot read CSV on its own.
NodeA conversion inside a service you already run.Needs a parser package. The built-ins have none.

In the browser

Paste or drop the file, check the delimiter it reports, take the JSON. Nothing is uploaded, which matters when the file is a customer export and the alternative is a site that wants you to hand it over. The ceiling is memory: past about 10 MB you want one of the others.

Python, standard library

import csv, json

with open('data.csv', newline='', encoding='utf-8-sig') as f:
    rows = list(csv.DictReader(f))

with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(rows, f, indent=2, ensure_ascii=False)

Every value comes out as a string, which is the correct default: the file did not say otherwise. newline='' is not optional, because the csv module handles line endings itself and will split records containing line breaks without it. ensure_ascii=False keeps accented characters as themselves rather than as escape sequences.

To convert only some columns to numbers, do it explicitly and one column at a time:

for row in rows:
    row['qty'] = int(row['qty'])
    row['price'] = float(row['price'])

pandas

import pandas as pd

pd.read_csv('data.csv', dtype=str).to_json('data.json', orient='records', indent=2)

dtype=str is the important argument. Without it pandas infers a type per column, and a column of order numbers becomes floats, so 00412 comes out as 412.0 and a missing value becomes NaN, which is not valid JSON. Drop dtype=str only when every column really is a measurement.

Miller, or jq

Miller reads CSV natively, which jq does not:

mlr --icsv --ojson cat data.csv > data.json

Going the other way, jq is the usual tool, and this is the incantation:

jq -r '(.[0] | keys_unsorted), (.[] | [.[]]) | @csv' data.json

It takes the keys of the first record as the header, then every record's values in order. Which means it is wrong the moment two records have different keys, and it will not say so. Miller's --ojson and --ocsv handle that case properly and are worth the install if you do this more than once.

Node

import { readFileSync, writeFileSync } from 'node:fs';
import { parse } from 'csv-parse/sync';

const rows = parse(readFileSync('data.csv'), { columns: true, bom: true });
writeFileSync('data.json', JSON.stringify(rows, null, 2));

Node ships no CSV parser, so this needs csv-parse. Use the streaming API rather than /sync for anything large. bom: true handles the Excel case from the encoding section above.

Which shape of JSON do you want

There are three, and they are not interchangeable.

  • An array of objects. The default, and what nearly everything expects. Each record carries its column names, so it costs more bytes and reads without a schema.
  • An array of arrays. The header stays as row one and the rest are positional. Compact, and the right shape when the column names are noise or the consumer is a chart library.
  • NDJSON. One object per line, no wrapping array. The only one of the three that can be read without holding the whole file, and the only one you can append to.

Check the result before you ship it

Convert it back. Turn the JSON into CSV again and compare it with the file you started with: anything that changed is something the conversion did to your data rather than to its formatting. Row counts that disagree usually mean a quoted line break, and a column that lost its leading zeros means a type guess you did not want.

Then read the JSON itself. Long IDs turning into rounded numbers and duplicate keys from two columns with the same header are both invisible in a diff and both obvious in a validator.

Related

Frequently asked questions

Is there an official CSV standard?

Not a binding one. RFC 4180 was published in 2005 and describes what most tools do: fields separated by commas, quotes around a field that contains a comma, a quote or a line break, a doubled quote standing for one, and CRLF line endings. It is informational, not a standard, and it was written years after everyone had already shipped. That is why two programs can both be right about the same file and disagree about what is in it.

Why does Excel change my data when it opens a CSV?

Because it guesses a type for every cell and it cannot be told not to. A leading zero is dropped, so 00123 becomes 123. Anything that looks like a date is converted, which is how the gene SEPT1 became 1-Sep and why the naming committee renamed dozens of human genes in 2020. Long numbers become scientific notation. None of this happens on import from Power Query with every column set to Text, and none of it happens in a text editor.

Why do the accents in my CSV come out as é?

The file is UTF-8 and something is reading it as Windows-1252. Each accented character is two bytes in UTF-8, and read one byte at a time they become two Latin-1 characters, so é becomes é. This is nearly always Excel. Adding a byte order mark to the file fixes it, because Excel treats a BOM as proof of UTF-8. In Python you read a BOM-prefixed file with encoding='utf-8-sig', which strips it.

What is NDJSON, and when do I want it?

One JSON value per line, with no wrapping array and no commas between records. Also called JSON Lines. You want it when the data is bigger than memory or arrives as a stream: a reader handles one line at a time and never holds the whole document, and appending a record is appending a line. A normal JSON array has to be parsed whole, so a 4 GB export needs 4 GB of memory before you can read the first record.

Should an API return CSV or JSON?

JSON, unless the answer is a table someone is going to open in a spreadsheet. JSON carries types and nesting, which CSV cannot, and every client already has a parser that agrees with every other client. CSV is worth offering as a second format on an export endpoint, because it is what a finance team can actually use. Offering only CSV pushes the typing problem onto every consumer.

How do I convert a CSV too big to open?

Stream it, and write NDJSON rather than one array. Python's csv.DictReader yields a row at a time, so writing json.dumps(row) and a newline per row never holds more than one record. Miller and jq are both streaming too. The thing to avoid is any approach that reads the file into a list first, which is what a browser tool and most one-liners do.

Can one CSV file hold two tables?

Not in any way another program will understand. Files with a blank line and a second header row halfway down exist, usually exported from a report, and every parser reads them as one ragged table. Split them into two files before converting. If you cannot, read the file as rows rather than records, find the index of the blank line, and convert each half separately.

move openesc close