CSVFormat CSV Converter

Where This Parser Disagrees With RFC 4180, Measured

CSVFormat — CSV Formatter & Converter Guides · Updated 2026-10-12 · All guides

RFC 4180 is short enough to implement exactly, and this parser is close to it. Close is where the interesting failures live. I ran inputs through the function that actually ships — extracted from the deployed page's own <script>, not a copy — and every output below is pasted verbatim from that run.

How to re-run this

The app logic is inline in apps/csvformat/public/index.html. The harness loads it unmodified against a DOM stub, so there is no fork between what is fuzzed and what is published:

// from scripts/fuzz/
const { loadApp } = require("./load");
const { api } = loadApp("csvformat", ["csvToJson", "csvToSql"]);
const toJson = s => { api.__setInput("in", s); api.csvToJson(); return api.__readOutput("out"); };
const toSql  = s => { api.__setInput("in", s); api.csvToSql();  return api.__readOutput("out"); };
console.log(toJson(`a,b\n"1,2",3`));

What it gets right

The parts that matter most are correct, and worth stating before the complaints. Quoted delimiters, escaped quotes, and CRLF all behave to spec:

InputJSON output
a,b ⏎ "1,2",3{"a":"1,2","b":"3"}✅ quoted comma
a ⏎ "say ""hi"""{"a":"say \"hi\""}✅ doubled quote
a,b ⏎ 1,2 (CRLF){"a":"1","b":"2"}✅ \r dropped

The six deviations

1. Cells are trimmed — including inside quotes

parseCsv finishes with clean.map(r => r.map(c => c.trim())), applied to every cell after parsing. RFC 4180 is explicit that whitespace inside a quoted field is part of the field.

InputOutputRFC 4180 says
" x "x" x " — preserve
"\tx\t"x\tx\t — a tab is not whitespace to strip
" " (spaces only)""" " — three spaces

The tab case is the one that bites. If a cell holds pre-formatted text — a code snippet, a TSV column, a fixed-width excerpt — converting it through this tool flattens it, and flattens it identically on every run, so it looks like the source was always flat.

There is no flag for this. I checked the comparison case rather than assume it: PapaParse 5.7.0 with default options preserves the same input ([["a","b"],[" x "," y "],[" p ","q"]]), so the trimming here is a deliberate local choice, not what "a normal CSV library" does. If exact whitespace matters, that is the library to reach for; if you want this tool, trim awareness is on you.

2. A semicolon file becomes one column

No delimiter sniffing, no delimiter option. a;b;c ⏎ 1;2;3 returns:

[{ "a;b;c": "1;2;3" }]

One key, one value, no warning. EU-locale spreadsheets export semicolon-delimited by default, so this is a common input, not an edge case. The stats line does not change shape either — it still reports a row count, which is what makes it easy to miss.

3. Ragged rows produce SQL that will not parse

Input a,b,c ⏎ 1,2 — three headers, two values. The emitted statement is:

INSERT INTO records (a, b, c) VALUES (1, 2);
-- WARNING: 1 row(s) did not match the header column count

Three columns, two values. That is a syntax error in PostgreSQL, MySQL, and SQLite, so the warning is doing real work — but note what it means for the file as a whole: the statement is not executable, so you cannot paste this and fix one row. The JSON path is gentler and pads the short row with "", so the same input gives you a usable object and a broken script. Pick your output format by how you intend to check it.

4. Numeric coercion is lossy, and quoting does not protect you

The SQL emitter decides per value whether to quote, using a numeric test rather than the parsed field's origin. Measured:

Input cellEmittedLost
012341234leading zero
"01234" (quoted in source)1234quoting did not help
90071992547409939007199254740992last digit — Number() precision
1.501.5significant-zero intent
+447700900123447700900123leading +

The quoted case is the important one. In CSV, quoting a field is exactly how a producer says "this is text, not a number" — it is why Excel writes "01234". That signal is discarded because trimming has already removed the quotes before the numeric test sees the value. Zip codes, SKU codes, phone numbers, IBANs, and any ID above 2^53 all arrive as quoted strings and all come out as numbers.

The JSON path is unaffected: "01" stays the string "01". The loss is specific to the SQL emitter.

5. NULL and the empty cell

InputEmittedNote
NULL (bare text)'NULL'quoted — safe, arguably correct
empty cell''empty string, not NULL

Treating a literal NULL as a string is the defensible choice — a CSV cell containing the four letters N-U-L-L is usually data. But an empty cell becoming '' rather than NULL is a silent semantic commitment: if your target table has NOT NULL defaults or you intended a null insert, '' will load without complaint and change meaning. There is no option for this; post-process if it matters.

6. Header handling

A header containing a space is emitted double-quoted — "first name" — which is correct for Postgres and SQLite and wrong for default MySQL, where double quotes mean a string literal unless ANSI_QUOTES is on. Identifiers are not otherwise escaped, so a header containing a double quote or a backtick is emitted raw.

What I would do with this

  • JSON out is the safe path. No coercion, no SQL syntax, quoting preserved except for trimming.
  • Before SQL out, look at any cell that is digits with a leading zero, a +, or more than 15 characters. Those four cases are where the emitter changes your data.
  • Never feed it a semicolon file without replacing ; with , first, and check the row count against your column count afterwards — one key in the JSON means sniffing failed, not that your file has one column.
  • If exact whitespace matters, this tool is the wrong tool, and no setting will make it right.

The deviations are consistent and small, which is what makes them worth documenting instead of fixing quietly: every one of them produces plausible-looking output.

Developer Sponsor / Partner
Copied to clipboard!