codr.tools
Tools

CSV to JSON converter

Convert CSV to JSON and JSON back to CSV in your browser, following RFC 4180, with the delimiter detected, quoted fields and line breaks handled, optional type inference, and errors reported by line and column.

Direction
Direction
In345 bytes
Out642 bytes
Swap
Ratio×1.86
StatusValid

Delimiter detected: comma.

"postcode" holds both numbers and text. A field with a leading zero, or with more digits than JSON can hold, stays text so that no digit changes.

Table preview

3 records · 7 columns · comma separated

The CSV side of the conversion. 3 records · 7 columns · comma separated.
idnamecitypostcodejoinedactivenote
1Ada LovelaceLondon, EnglandNW1 6XE1843-12-10trueWrote the notes on the Analytical Engine
2Hopper, GraceNew York101181906-12-09trueSaid: "It's easier to ask forgiveness than permission"
3Katherine JohnsonNewport News021341918-08-26falseTwo lines: the second one, with a comma
Reading

With the header off you get an array of arrays, one per record. A quoted field always stays text, and so do numbers with a leading zero or more digits than JSON can hold, so postcodes and long ids keep every digit.

Convert CSV to JSON and JSON back to CSV in your browser, following RFC 4180, with the delimiter detected, quoted fields and line breaks handled, optional type inference, and errors reported by line and column.

How to use this tool#

  1. Pick a direction. CSV to JSON parses the text and writes JSON; JSON to CSV goes back the other way. The conversion runs as you type, so there is no button to press.
  2. Paste your file over the example. The example is there so you can see what comes out before you commit anything of your own.
  3. Set how the CSV should be read. The delimiter is detected by default, and you can force comma, semicolon, tab or pipe. Turn off First record holds the column names to get an array of arrays, and turn off type inference to keep every field as a string.
  4. Read the messages under the output. A syntax error names the line and the column and shows the line with a caret under the spot. Notes appear for anything worth knowing: the delimiter that was detected, a byte order mark that was stripped, records that are not the width of the header.
  5. Check the table preview. It shows the CSV side as a grid, which is the quickest way to see that a stray quote has not welded two columns together.
  6. Use Swap for a round trip. It puts the output back in the input and flips the direction, so you can see what survives the journey.

The tool doubles as a CSV validator: if the text parses, the JSON appears; if a quote is left open or a quoted field is followed by stray text, you get the position of the first problem and the records that could still be read. Nothing is uploaded, so an export full of customer data is safe to paste.

CSV is a family of formats, not one format#

The closest thing to a specification isRFC 4180, published in October 2005. It is an Informational memo, not a standard: "This memo provides information for the Internet community. It does not specify an Internet standard of any kind." It opens by admitting the problem it is documenting: "there is no formal specification in existence, which allows for a wide variety of interpretations of CSV files."

Stripped to the parts that decide how a file is read, the RFC says:

  • Each record sits on its own line, separated by CRLF, and the last record may leave the final line break off.
  • An optional header line comes first, with the same shape as a record.
  • Fields are separated by commas, every line should hold the same number of fields, and "Spaces are considered part of a field and should not be ignored."
  • Any field may be wrapped in double quotes. "If fields are not enclosed with double quotes, then double quotes may not appear inside the fields."
  • "Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes."
  • "If double-quotes are used to enclose fields, then a double-quote appearing inside a field must be escaped by preceding it with another double quote."

The advice the RFC gives implementers is the robustness principle: "be conservative in what you do, be liberal in what you accept from others." That is what this tool does. It writes files that fit the grammar above, and when reading it also accepts a bare LF or a lone CR as a record separator, a semicolon, tab or pipe as the delimiter, a UTF-8 byte order mark at the front, and records that are not all the same width. Anything it had to tolerate is reported as a note under the output rather than swallowed.

RFC 4180 has been updated once, byRFC 7111 in 2014. It leaves the grammar alone and adds URI fragments that address part of a file (#row=4,#cell=4,2), plus one useful sentence about encoding: when thecharset parameter is absent, "UTF-8 SHOULD be assumed as the default".

How a table becomes JSON#

A CSV file is a rectangle of text. JSON is a tree. The conversion is a choice about which JSON shape stands for that rectangle, and the header option is what picks it.

text
CSV, with a header record

id,name,city,active
1,Ada,"London, England",true
2,Grace,New York,false


JSON, header on: an array of objects

[
  { "id": 1, "name": "Ada", "city": "London, England", "active": true },
  { "id": 2, "name": "Grace", "city": "New York", "active": false }
]


JSON, header off: an array of arrays

[
  ["id", "name", "city", "active"],
  ["1", "Ada", "London, England", "true"],
  ["2", "Grace", "New York", "false"]
]

With the header on, the first record supplies the keys, in the order it lists them, and every later record becomes one object. With it off, nothing is treated as special and you get an array of arrays, which is the right shape when the file has no header or when column order matters more than column names.

Two header cases need a rule, because JSON cannot express what the CSV is asking for:

  • An empty header cell. The column is named after its position:column_2. A note says so.
  • A repeated header name. An object cannot hold the same key twice, so the last column with that name wins and the tool tells you. Python's csv.DictReader does the same thing silently.

Records that are not the width of the header are a warning, not an error, because they are usually the interesting part of a broken export. Nothing is dropped: a short record is padded, and a field past the end of the header is kept under a positional name.

text
id,name,city
1,Ada
2,Grace,New York,extra

becomes

[
  { "id": 1, "name": "Ada", "city": null },
  { "id": 2, "name": "Grace", "city": "New York", "column_4": "extra" }
]

Type inference, and the columns it ruins#

CSV has no types. Every field in the file is text, and any converter that hands you a number has guessed. The guess is usually what you want and occasionally destroys the column, so it is a checkbox here, and the rules it follows are narrow on purpose.

text
Field        Type inference on      Why

42           42                     a JSON number
-1.5e3       -1500                  a JSON number
true / FALSE true / false           either case
null         null                   either case
(empty)      null                   nothing there
"42"         "42"                   quoted, so text
02134        "02134"                a leading zero is data, not a number
9007199254740993
             "9007199254740993"     more digits than JSON can hold
+1  .5  1.   "+1" ".5" "1."         not the JSON number grammar
1,5          "1" and "5"            a comma is still a delimiter
2026-09-20   "2026-09-20"           a date is text, here and in JSON

A field becomes a number only if it matches JSON's own number grammar. That grammar has no leading plus, no leading zero, no bare .5 and no trailing 1., which is exactly the set of cases where guessing goes wrong:

  • Leading zeros are data. Postcodes, phone numbers, account numbers and product codes all lose their meaning as soon as 02134 becomes 2134, so they stay text.
  • Long integers stay text. Past 253 a JSON reader built on doubles changes the digits. A 19-digit id would come back as a different id, so the tool keeps it as a string rather than quietly rewriting it.
  • Quoting protects a value. A field written inside double quotes is never inferred. "42" stays the string "42". That is the escape hatch when a single column mixes codes and numbers, and it is why the Quote every fieldoption on the way out makes a round trip through this tool lossless.
  • Dates are never parsed. JSON has no date type, so 2026-09-20comes out as the string it already was.

Other tools draw the line elsewhere. Papa Parse's dynamicTyping is documented as "numeric and boolean data will be converted to their type instead of remaining strings", with no exception for a leading zero. Python's csv module never guesses at all unless you ask for QUOTE_NONNUMERIC, which makes the reader "convert all non-quoted fields to type float". If you are comparing output between tools, this is usually where they differ.

When inference is on, the tool names any column that ended up holding both numbers and text. That is almost always a column of codes where some values start with a zero and some do not, and it is worth seeing before the file goes anywhere else.

Quotes, commas and line breaks#

Quoting is the only thing in CSV that carries any structure, and it does three jobs at once: it lets a field hold the delimiter, a line break, or a quote of its own. A quote inside a quoted field is written twice, which is the part most hand-written exporters get wrong.

text
id,note
1,"Paris, France"                 a delimiter inside a field
2,"She said ""no"" twice"          a quote, written twice
3,"first line
second line"                      a record spanning two lines
4, padded ,                       spaces are part of the field
5,"",                             an empty field, quoted and not

Two mistakes are common enough that this tool reports them rather than guessing:

  • An unclosed quote. Everything after the opening quote, to the end of the file, is one value. The error points at the quote that opened it, not at the end of the file, because that is the character you have to fix. The records read before it are still shown.
  • Text after a closing quote. "a"b is not valid: the field ended at the second quote. It usually means an inner quote was written once instead of twice. The text is kept in the value so nothing disappears, and the position is reported. Python's reader accepts the same input silently and gives you ab.
text
id,note
1,"unclosed
2,ok

A quoted field is never closed. Everything after the opening
quote was read as one value. A quote inside a quoted field has
to be written twice ("") (line 2, column 3)

2 | 1,"unclosed
  |   ^

A quote in the middle of an unquoted field, as in 6" pipe, is kept as an ordinary character. The RFC forbids it, every real parser tolerates it, and there is no other sensible reading.

Delimiters: comma, semicolon, tab, pipe#

The comma in the name is optimistic. Plenty of real files are separated by semicolons, tabs or pipes, and the tool detects which by parsing the first records with each candidate and keeping the one that yields the most columns while keeping every record the same width. Quoted text is parsed properly during that test, so a comma inside "Paris, France" does not vote. A tie goes to the comma.

The usual reason a file is not comma-separated is Excel. Microsoft's own troubleshooting documentation states that "Excel utilizes the Windows list separator character for the delimiter in .csv files", and that "the comma is the default list separator for US - English Locale". On a machine whose locale uses the comma as a decimal mark, that list separator is a semicolon, and the exported file follows.

Some of those exports start with a sep=; line, which tells Excel which delimiter to expect. It is not part of RFC 4180 and Microsoft's own documentation does not mention it, so this tool treats it as an ordinary record. Delete that line before pasting, or set the delimiter by hand.

text
sep=;
id;name
1;Ada

Tab-separated files have their own registered media type,text/tab-separated-values, and its registration is stricter than CSV in one way: "fields that contain tabs are not allowable in this encoding". There is no escape hatch, because the format has no quoting. When this tool writes a tab-separated file it quotes a field containing a tab the way it would for a comma file, which most readers accept but which the TSV registration does not describe. If the receiving system is strict, strip tabs from your data first.

Byte order marks, encodings and line endings#

A UTF-8 byte order mark is three bytes (EF BB BF) at the very start of a file. It is what makes a spreadsheet open a UTF-8 export as UTF-8, and it is also what turns your first column name into something that no longer matches "id" in code. This tool strips it before parsing and says so in a note, and the JSON to CSV direction can add one back with a checkbox.

If a file arrives full of é and ’ it is not a CSV problem: the bytes are one encoding and something read them as another. RFC 7111 says UTF-8 should be assumed when nothing says otherwise, but old exports are often Windows-1252.

shell
Reading a file whose encoding you do not know:

file -i data.csv            # what the system thinks it is
head -c 3 data.csv | xxd    # "ef bb bf" is a UTF-8 byte order mark
iconv -f WINDOWS-1252 -t UTF-8 data.csv > utf8.csv

On line endings, the RFC asks for CRLF and allows the last record to leave it off. This tool reads CRLF, LF and lone CR without being told which, and writes LF unless you tickEnd records with CRLF. A line break inside a quoted field is kept exactly as it was written, so a CRLF file with a multi-line note gives you a JSON string containing\r\n.

If you write CSV from Python, the encoding of those line breaks is the one thing the standard library insists on: "If csvfile is a file object, it should be opened withnewline=''", because otherwise "newlines embedded inside quoted fields will not be interpreted correctly, and on platforms that use \r\n line endings on write an extra \r will be added".

JSON to CSV: the decisions a converter has to make#

Going back the other way, a tree has to be flattened into a rectangle, and there is no single right answer. Here is what this tool decides, so you can tell whether it matches what you need.

Which objects define the columns. The header is the union of every key that appears, in the order the keys are first seen, not just the keys of the first object. An object that lacks a key gets an empty cell, and a note appears when the objects are not uniform.

text
[
  { "id": 1, "name": "Ada" },
  { "id": 2, "city": "Bletchley" }
]

becomes

id,name,city
1,Ada,
2,,Bletchley

What to do with a nested value. CSV has no nesting. Either the value keeps its JSON text inside the cell, quoted the way the format requires, or the object is flattened so that each leaf gets its own column, with a dot for a key and [n] for an array index. JSON text is reversible and ugly; flattening is readable and one way only, since nothing in the CSV says which dots were nesting.

text
[
  { "id": 1, "tags": ["a", "b"], "address": { "city": "Paris", "zip": "75001" } }
]

Keep as JSON text in the cell

id,tags,address
1,"[""a"",""b""]","{""city"":""Paris"",""zip"":""75001""}"

Flatten into their own columns

id,tags[0],tags[1],address.city,address.zip
1,a,b,Paris,75001

What to do with null. CSV has no null either. null and the empty string both become an empty cell, and the difference between them is gone for good. Reading that cell back gives you null with type inference on and "" with it off, which is the honest answer: the file no longer knows.

The shapes that are accepted. An array of objects, an array of arrays, an array of plain values (one column named value), or a single object, which becomes a header and one record. An array that mixes shapes is an error naming the element that broke the pattern, because guessing there would silently lose rows.

Before you open it in a spreadsheet#

A CSV file is correct or incorrect on its own terms, but a spreadsheet will apply its own reading on import. Two things are worth knowing before you blame the converter.

Quoting is grammar, not type. Double quotes tell a CSV parser where a field ends. They carry no information about what the field is, so wrapping 02134 in quotes does not stop an importer that wants to see a number there. If leading zeros or long ids have to survive, import the column as text explicitly, or hand the data over as JSON instead.

A cell that starts with = is a formula. OWASP documents this asCSV Injection, also called formula injection: a field beginning with =, +,-, @, a tab (0x09) or a carriage return (0x0D) can be executed by spreadsheet software when the file is opened, which is a real attack when the data came from your users. The mitigation is to stop such a field from starting with those characters in the code that writes the file. This tool deliberately does not add that escaping, because it would change your data without asking; if you are generating files from untrusted input, do it where you build the rows.

Converting on the command line#

Python

The standard library covers both directions with no install, and its reader is closer to RFC 4180 than most one-liners.

python
import csv, json, sys

# CSV to JSON
print(json.dumps(list(csv.DictReader(sys.stdin)), indent=2))

# JSON to CSV, with the union of the keys as the header
rows = json.load(sys.stdin)
cols = list({key: None for row in rows for key in row})
writer = csv.DictWriter(sys.stdout, fieldnames=cols)
writer.writeheader()
writer.writerows(rows)
shell
$ printf 'id,name,city\n1,Ada,"London, England"\n' | python3 -c \
    'import csv,json,sys; print(json.dumps(list(csv.DictReader(sys.stdin)), indent=2))'
[
  {
    "id": "1",
    "name": "Ada",
    "city": "London, England"
  }
]

# Every value is a string: the csv module does not guess types.
# The other direction, with a key missing from the second object:
$ echo '[{"id":1,"name":"Ada"},{"id":2,"city":"NY"}]' | python3 json_to_csv.py
id,name,city
1,Ada,
2,,NY

jq

jq writes CSV well and does not read it at all. @csv quotes strings, leaves numbers bare and doubles an inner quote; there is no matching CSV parser on the input side, which is why the hand-rolled split(",") version falls apart on the first quoted field.

shell
# jq writes CSV with @csv, which quotes strings and escapes
# an inner quote by doubling it.
$ echo '[{"id":1,"name":"Ada"},{"id":2,"name":"Grace"}]' | \
    jq -r '(.[0] | keys_unsorted) as $cols | $cols, (.[] | [.[$cols[]]]) | @csv'
"id","name"
1,"Ada"
2,"Grace"

# jq has no CSV reader. Splitting the text by hand is fine until a
# field holds a comma or a line break:
$ printf '1,"Paris, France"\n' | jq -R 'split(",")'
[
  "1",
  "\"Paris",
  " France\""
]

Questions people ask#

Is this an RFC 4180 parser?

It writes RFC 4180 and it reads a superset of it. The extras are the ones every real file needs: LF and lone CR line endings, delimiters other than the comma, a UTF-8 byte order mark, and records that are not all the same width. The two things the RFC forbids that this tool reports rather than accepts quietly are an unclosed quote and text after a closing quote.

Can I convert JSON back to CSV here?

Yes. Switch the direction to JSON to CSV, or press Swap after a conversion. It takes an array of objects, an array of arrays, an array of plain values or a single object.

My file is semicolon-separated and everything lands in one column.

Detection needs at least two columns' worth of evidence in the first records. If the file starts with a sep=; line, or the first record happens to hold no semicolon, set the delimiter by hand with the Delimiter dropdown.

Why did my postcode lose its leading zero?

Not here: 02134 stays a string in this tool whether or not type inference is on. Elsewhere it is usually a spreadsheet import or a converter that runs every field through a number parser. Turning inference off, or quoting every field on the way out, is how you keep the column intact through the next tool.

What happens to an empty cell?

With type inference on it becomes null, with inference off it becomes an empty string. A quoted empty field ("") is always the empty string, since quoting means text. Going the other way, both null and "" are written as an empty cell, because CSV has no way to tell them apart.

Can it produce nested JSON from a CSV?

No. A CSV row is flat, so the output is flat: one key per column. A column calledaddress.city stays a key called address.city. Flattening in the other direction is supported and is deliberately one way, because nothing in a CSV file marks which dots used to be nesting.

Does it handle TSV?

Yes. Choose Tab as the delimiter, or let detection find it. Pasting a tab-separated file into the box works as long as your browser pastes real tab characters, which it does from a file or an editor; some web pages hand out spaces instead.

How large a file can it take?

The limit is your machine, not an upload size, because there is no upload. A few megabytes is comfortable. The output box shows the first 200,000 characters to keep scrolling smooth while copy gives you all of it, and the table preview shows the first 20 records of any file.

Does anything leave my browser?

No. The parser is JavaScript running on this page, the site is static files with no backend, and there is nowhere for the data to be sent. Disconnect from the network and the tool keeps working, which is the quickest way to prove it. That also makes it safe for an export full of customer records, which is most of what people paste into a CSV converter.

What else is on the site for this?

JSON formatter for indenting and validating the JSON you get out of here, YAML to JSON for the other configuration format, andcase converter for turning a header row ofFirst Name columns into first_name keys.

  • Format, minify and validate JSON in your browser, with syntax errors explained by line and column, repeated keys listed, and number literals kept exactly as written.

  • Convert YAML to JSON and JSON back to YAML in your browser, following YAML 1.2, with anchors and merge keys resolved, multi-document streams, and syntax errors reported by line and column.

  • Format and beautify SQL in your browser across 20 dialects, from PostgreSQL and MySQL to SQL Server, BigQuery and Snowflake, with your own indent width and keyword case, plus a minifier that puts a query back on one line.

  • Convert identifiers and text to camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, Train-Case, dot.case, path/case and four more at once, one line at a time, with acronyms and digits split the way style guides expect.