How Papa Parse Turns CSV Rows into JSON
Docify transforms CSV into JSON in the browser by calling Papa.parse with header: true so each data row becomes an object keyed by the first row. Completely empty lines are skipped (testEmptyLine is s.length === 1 && s[0].length === 0); a blank line between Ada,30 and Bob,40 converts both rows, while a spaces-only row is UndetectableDelimiter plus TooFewFields. Numbers that look like decimal literals and true / false are typed. A padded ID such as 007 becomes the number 7 because dynamicTyping uses Papa's FLOAT test, then parseFloat. Quoted CSV "007" is still 7. 007a stays a string. A plus-prefixed cell such as +30 stays the string "+30" because Papa's FLOAT test accepts an optional minus, not a plus. -30 becomes the number -30. Plus is only legal in an exponent (1e+2). An incomplete exponent such as 1e or 1e+ stays the string "1e" or "1e+" because Papa's FLOAT exponent needs one or more digits after e or E. Write 1e+2 or 100 for the number. JSON.parse of "1e" throws (RFC 8259 §6). A hexadecimal prefix such as 0x1 stays the string "0x1" because Papa's FLOAT regex has no 0x. Write 1 for the number. JSON.parse of "0x1" throws (RFC 8259 §6). An underscore separator such as 1_000 stays the string "1_000" because Papa's FLOAT regex has no underscore. Write 1000 for the number. JSON.parse of "1_000" throws (RFC 8259 §6). A leading or trailing decimal such as .5 or 1. becomes the number 0.5 or 1 because Papa's FLOAT test allows those forms. JSON.parse rejects them (RFC 8259 §6). Quoted ".5" is still 0.5. A cell at ±2^53 such as 9007199254740992 stays the string "9007199254740992" because Papa's testFloat keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT (MAX_FLOAT is 2^53). 9007199254740991 becomes a number. A complete exponent such as 1e16 stays the string "1e16" because parseFloat of "1e16" is 10000000000000000, which is not < 2^53. Write 1e15 for the number 1000000000000000. A complete tiny exponent such as 1e-324 becomes the number 0 because parseFloat of "1e-324" is 0 (IEEE-754 underflow; Number.MIN_VALUE is 5e-324), and 0 is inside ±2^53. Write 5e-324 for Number.MIN_VALUE. JSON.parse of "9007199254740992" is the number 9007199254740992. A FLOAT cell with surrounding space such as 30 becomes the number 30 because Papa's FLOAT regex is /^\s*…\s*$/. parseFloat of " 30 " is 30. Quoted " 30 " is still 30. true stays a string because the boolean test is exact equality. JSON.parse of " 30 " is also 30 (RFC 8259 insignificant whitespace). An unquoted both-side tab around 30 on a one-column header is auto-detected as a delimiter (TooManyFields); the live page writes no JSON. A quoted tab-padded 30 stays one cell and becomes the number 30 because Papa's FLOAT \s includes tab. parseFloat of "\ 30\ " is 30. In a two-column comma table, an unquoted tab-padded 30 is also 30 because comma already won. A one-column table such as name then Ada is UndetectableDelimiter because guessDelimiter keeps a candidate only when the average field count is greater than 1.99. Papa still defaults to comma and would return [{"name":"Ada"}], but the live page treats any result.errors as fatal and writes no JSON. Two-column n,x plus 30,foo succeeds. Duplicate headers are renamed so object keys are not overwritten: the first name stays name; a later name becomes name_1. name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"} and Convert writes that JSON. Quoted "name" / "name" is the same. name and Name are different keys. JSON.parse of {"name":"Ada","name":"Bob"} keeps the last value (RFC 8259 §4). A short data row such as name,age,city plus Ada,30 is TooFewFields (expected 3 fields but parsed 2). Papa still builds {"name":"Ada","age":30} and omits city, but the live page treats that FieldMismatch as fatal and writes no JSON. A trailing comma (Ada,30,) is three fields ({"name":"Ada","age":30,"city":null}) and Convert succeeds. A long row such as name,age plus Ada,30,extra is TooManyFields; Papa stores the extra cell on __parsed_extra as ["extra"]. An unclosed quote such as name,note plus Ada,"hello is MissingQuotes (Quoted field unterminated). Papa still builds {"name":"Ada","note":"hello"}, but the live page treats that Quotes error as fatal and writes no JSON. A closed Ada,"hello" is the same object and Convert writes the JSON. A trailing space after a closing quote such as Ada,"hello" plus a space is InvalidQuotes (Trailing quote on quoted field is malformed) plus MissingQuotes. Papa still builds {"name":"Ada","note":"hello\" "}, but the live page treats those Quotes errors as fatal and writes no JSON. A doubled quote inside a quoted field such as Ada,"hel""lo" is RFC 4180 §7 (the "" pair is one quote, ABNF 2DQUOTE). Convert writes {"name":"Ada","note":"hel\"lo"} and there is no Quotes error. A single inner quote without doubling (Ada,"hel"lo") is InvalidQuotes; Papa still builds the same-looking note, but the live page writes no JSON. A backslash is not that escape. A two-column semicolon table such as name;age then Ada;30 is auto-detected (guessDelimiter keeps a candidate when the average field count is greater than 1.99). Convert writes [{"name":"Ada","age":30}]. Papa's FLOAT test uses a decimal point, so 3,14 in that table stays the string "3,14". Write 3.14 for the number 3.14. Mixed comma and semicolon in the same table is UndetectableDelimiter; the live page writes no JSON. A full ISO timestamp such as 2026-09-18T12:00:00Z becomes 2026-09-18T12:00:00.000Z because dynamicTyping uses Papa's ISO_DATE test, then JSON.stringify calls Date.prototype.toJSON. A date-only cell stays a string. The strings true and TRUE become JSON true; false and FALSE become JSON false. Title-case True and False stay strings because Papa's boolean test is exact equality, not case-insensitive. An empty cell, and a quoted empty "", become JSON null because after those tests parseDynamic returns value === '' ? null : value. A space-only cell and the word null stay strings. A completely empty line between Ada,30 and Bob,40 is skipped; Convert writes both rows. A quoted empty row "" is also skipped. A spaces-only row is kept, drops the average field count below 1.99, and is UndetectableDelimiter plus TooFewFields; the live page writes no JSON. A comma-only row , is two empty fields ({"name":null,"age":null}) and Convert succeeds. A two-column hash line such as # note,ignored is kept as data (comments defaults to false; RFC 4180 has no comment syntax). Convert writes {"name":"# note","age":"ignored"} plus the Ada row. #Ada,30 plus Bob,40 is the same with a typed 30. A one-field # comment row is UndetectableDelimiter plus TooFewFields; the live page writes no JSON. comments: true (or comments: '#') would skip a line that starts with #; the page does not set that option. The delimiter is auto-detected. Nothing is uploaded.
A spreadsheet row is still just a line of text until Convert runs. The CSV to JSON converter does not stream rows, does not infer types beyond Papa’s dynamicTyping, and does not keep a schema. For the buttons and the sample table, start with CSV to JSON with Papa Parse.
What each option does
header: true
The first row is field names. Later rows become objects, not arrays. Duplicate headers are renamed so keys are not overwritten: the first name stays name; a later name becomes name_1. name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"}. A header-less dump is not available on this page.
skipEmptyLines: true
Papa's testEmptyLine for the boolean is s.length === 1 && s[0].length === 0— a single empty field. A blank line between Ada,30 and Bob,40 is dropped, so Convert writes both rows. A quoted empty row "" is also skipped. A spaces-only row is kept, drops the average field count below 1.99, and is UndetectableDelimiter plus TooFewFields. A comma-only row , is two empty fields ({"name":null,"age":null}) and Convert succeeds. Greedy skip (skipEmptyLines: 'greedy') is not turned on.
dynamicTyping: true
Decimal literals strictly inside ±2^53 become numbers. The strings true and false become booleans. Title-case True and False stay strings. A padded ID such as 007 is a decimal literal, so it becomes 7. A plus-prefixed cell such as +30 stays a string. A leading or trailing decimal such as .5 or 1. becomes a number. The range check is exclusive, so 9007199254740992 stays a string. Surrounding whitespace on a FLOAT cell is allowed, so 30 becomes a number. An unquoted both-side tab around 30 on a one-column header is auto-detected as a delimiter (TooManyFields). A quoted tab-padded 30 stays one cell and becomes a number. A clean one-column table such as name / Ada is UndetectableDelimiter; Papa still builds [{"name":"Ada"}], but the live page writes no JSON. A date-only cell such as 2026-09-18 and European comma decimals stay strings (in a semicolon table, n;x plus 3,14;foo is {"n":"3,14","x":"foo"}; write 3.14 for the number). A two-column semicolon table such as name;age plus Ada;30 converts. A full ISO timestamp such as 2026-09-18T12:00:00Z becomes 2026-09-18T12:00:00.000Z. An empty cell becomes JSON null.
CSV 007 becomes JSON 7
Papa's FLOAT test accepts an optional minus, digits (leading zeros included), an optional decimal, and an optional exponent, then parseFloat returns a JavaScript Number when that value is inside ±(2^53). parseFloat("007") is 7, so zip,007 becomes {"zip":7}. 00123 becomes 123. 00 becomes 0. RFC 4180 quotes around "007" are stripped before that test, so a quoted 007 is still 7. 007a stays the string "007a". The live converter always enables dynamicTyping; there is no keep-as-string toggle. JSON has no leading-zero integer (RFC 8259 §6).
CSV +30 stays the JSON string +30
Papa's FLOAT test accepts an optional minus, digits (leading zeros included), an optional decimal, and an optional exponent — not a leading plus — then parseFloat returns a JavaScript Number when that value is inside ±(2^53). +30 is not that pattern, so n,+30 becomes {"n":"+30"}. RFC 4180 quotes around "+30" are stripped first, so a quoted +30 is still the string "+30". -30 becomes the number -30. 30 becomes 30. +30.5 and +007 stay strings. Plus is legal in an exponent, so 1e+2 becomes 100. +1e2 stays "+1e2". The live converter always enables dynamicTyping; there is no coerce-plus toggle. JSON has no plus-prefixed number (RFC 8259 §6).
CSV .5 and 1. become JSON 0.5 and 1
Papa's FLOAT test accepts an optional minus, then a leading decimal such as .5, a trailing decimal such as 1., or a complete decimal such as 0.5, plus an optional exponent, then parseFloat returns a JavaScript Number when that value is inside ±(2^53). parseFloat(".5") is 0.5, so n,.5 becomes {"n":0.5}. parseFloat("1.") is 1, so n,1. becomes {"n":1}. RFC 4180 quotes around ".5" are stripped first, so a quoted .5 is still 0.5. A quoted 1. is still 1. -.5 becomes the number -0.5. -1. becomes -1. 0.5 stays 0.5. 1.0 becomes 1. A lone . stays the string ".". 1.2.3 stays "1.2.3". .5e2 becomes 50. 1.e2 becomes 100. JSON.parse rejects .5 and 1. (RFC 8259 §6 requires an integer part and at least one digit after a decimal point). Write 0.5 or 1.0 in JSON. The live converter always enables dynamicTyping; there is no keep-as-string toggle.
CSV 9007199254740992 stays a JSON string
After the FLOAT regex matches, Papa's testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, where MAX_FLOAT is 2^53 (9007199254740992). Those bounds are exclusive, so 9007199254740991 becomes the number 9007199254740991 and n,9007199254740992 becomes {"n":"9007199254740992"}. RFC 4180 quotes around "9007199254740992" are stripped first, so a quoted 9007199254740992 is still the string "9007199254740992". -9007199254740991 becomes the number -9007199254740991. -9007199254740992 stays "-9007199254740992". 9007199254740993 stays "9007199254740993" because parseFloat of that digit string is 9007199254740992, which is not < MAX_FLOAT. Number.MAX_SAFE_INTEGER is 2^53 − 1. JSON.parse of "9007199254740992" is the number 9007199254740992 (exactly representable); JSON.parse of "9007199254740993" is 9007199254740992 (IEEE-754). The live converter always enables dynamicTyping; there is no BigInt path.
CSV 30 becomes JSON 30
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/, so surrounding whitespace is allowed. After that match, testFloat calls parseFloat and keeps a Number when that value is strictly inside ±2^53. parseFloat(" 30 ") is 30, so n, 30 becomes {"n":30}. RFC 4180 quotes around " 30 " are stripped first, so a quoted 30 is still 30. A leading-only space and a trailing-only space are also 30. -30 becomes -30. .5 becomes 0.5. 1. becomes 1. 1e+2 becomes 100. An internal space such as 3 0 stays "3 0". A space-only cell stays " ". Papa's boolean test is exact equality, so true stays " true ". Papa's ISO_DATE test is a full-string match, so 2026-09-18T12:00:00Z stays a string. If the exclusive-range check fails, the original cell is kept, so 9007199254740992 stays " 9007199254740992 ". 9007199254740991 becomes 9007199254740991. JSON.parse of " 30 " is also 30 (RFC 8259 insignificant whitespace). The live converter always enables dynamicTyping; there is no trim toggle.
An unquoted tab-padded 30 is TooManyFields
Papa leaves the delimiter blank, so guessDelimiter tries comma, tab, pipe, semicolon, and the ASCII record/unit separators, and keeps a candidate only when the average field count is greater than 1.99. Header n plus an unquoted tab, 30, and a tab is three fields, so tab wins and the row is TooManyFields (expected 1 fields but parsed 3). The live converter shows that message and writes no JSON. RFC 4180 quotes keep those tabs inside one cell, so tab is not guessed. Papa's FLOAT regex allows surrounding \s, and \s includes tab, so parseFloat("\\t30\\t") is 30. In a two-column comma table, n,x plus a quoted tab-padded 30 and foo becomes {"n":30,"x":"foo"}. The same two-column table with an unquoted tab-padded 30 is also 30 because comma already won. A leading-only tab or a trailing-only tab on a one-column header averages 1.5 fields, so tab is not chosen; Papa still types 30, but UndetectableDelimiter defaults to comma and the page still writes no JSON. A quoted tab-padded true stays a string. A quoted tab-padded ISO timestamp stays a string. A quoted tab-padded 9007199254740992 stays that tab-padded string. JSON.parse of a tab-padded 30 is also 30 (RFC 8259 insignificant whitespace). The live converter always enables dynamicTyping; there is no delimiter picker.
A one-column name / Ada is UndetectableDelimiter
Papa leaves the delimiter blank, so guessDelimiter keeps a candidate only when the average field count is greater than 1.99. Header name then Ada averages 1 field, so no delimiter wins. Papa still defaults to comma and result.data is [{"name":"Ada"}]. The live converter treats any result.errors as fatal (errors.length > 0), so UndetectableDelimiter is shown and the page writes no JSON. zip then 007 is the same warning; Papa would type [{"zip":7}] but the page still writes no JSON. Quoted "name" / "Ada" is the same. Two-column n,x plus 30,foo averages 2 fields, comma wins, there is no warning, and Convert writes [{"n":30,"x":"foo"}]. Adding a dummy second column (name,x then Ada,) becomes [{"name":"Ada","x":null}] because the empty cell is null, and Convert succeeds. This is not TooManyFields. A leading-only or trailing-only tab on one column is also UndetectableDelimiter. The live converter does not ignore delimiter warnings and has no delimiter picker.
A duplicate CSV header name becomes JSON name_1
Papa.parse runs with header: true. Duplicate field names are renamed so later columns do not overwrite earlier ones. The first name stays name; a later name becomes name_1, then name_2. name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"}. RFC 4180 quotes around "name" are stripped first, so a quoted "name","name" is the same. A third name becomes name_2 ({"name":"Ada","name_1":"Bob","name_2":"Cam"}). If name_1 already exists as a real header (name,name_1,name), the later name becomes name_2. name and Name are different keys (case-sensitive). A leading space (name, name) is a different key. Two empty headers become "" and _1. meta.renamedHeaders is { name_1: "name" }, but the live page does not expose that map or transformHeader. JSON.parse of {"name":"Ada","name":"Bob"} keeps the last value (RFC 8259 §4); Papa avoids that overwrite. There is no FieldMismatch; Convert writes the JSON. The live converter does not expose a header-rename toggle.
A short CSV row is TooFewFields and writes no JSON
Papa.parse runs with header: true. After each data row is built, Papa compares the parsed field count with the header count. name,age,city plus Ada,30 is TooFewFields (Too few fields: expected 3 fields but parsed 2). Papa still builds {"name":"Ada","age":30} and omits city (the missing key is not null). Convert treats any result.errors as fatal (errors.length > 0), so that FieldMismatch is shown and the live page writes no JSON. A one-cell row Ada is TooFewFields expected 3 parsed 1 ({"name":"Ada"}). RFC 4180 quotes around "Ada" do not change the count. A trailing comma (Ada,30,) is three fields: the empty city becomes JSON null and Convert writes {"name":"Ada","age":30,"city":null}. An empty middle cell (Ada,,NY) is also three fields ({"name":"Ada","age":null,"city":"NY"}). A long row such as name,age plus Ada,30,extra is TooManyFields (Too many fields: expected 2 fields but parsed 3); Papa stores the extra cell on __parsed_extra as ["extra"]. Two extras become ["extra","more"]. A quoted extra is the same. This is not the tab-padded one-column TooManyFields (that is delimiter detect). The live converter does not ignore FieldMismatch and has no partial-output mode.
An unclosed CSV quote is MissingQuotes and writes no JSON
Papa.parse runs with header: true. A field that starts with a quote must find a closing quote before EOF (RFC 4180). name,note plus Ada,"hello is MissingQuotes (Quoted field unterminated). Papa still builds {"name":"Ada","note":"hello"} (the opening quote is skipped; the rest of the input stays in note). Convert treats any result.errors as fatal (errors.length > 0), so that Quotes error is shown and the live page writes no JSON. A closed Ada,"hello" is the same object and Convert writes the JSON. A comma inside the unclosed quote (Ada,"hello,world) stays in note ({"name":"Ada","note":"hello,world"}). A closed multiline quote (Ada,"hello then a newline then world") is valid RFC 4180 and Convert writes {"name":"Ada","note":"hello\nworld"}. An unclosed quote that continues onto the next line (Ada,"hello then Bob,ok) swallows that line into note ({"name":"Ada","note":"hello\nBob,ok"}) and is still MissingQuotes. This is not FieldMismatch (that is TooFewFields / TooManyFields). The live converter does not ignore Quotes errors and has no partial-output mode.
A trailing space after a closing CSV quote is InvalidQuotes and writes no JSON
Papa.parse runs with header: true. After a closing quote, Papa accepts a delimiter, a newline, or spaces then a delimiter or newline (extraSpaces). A trailing space at EOF after Ada,"hello" is not those. name,note plus Ada,"hello" and a trailing space is InvalidQuotes (Trailing quote on quoted field is malformed) plus MissingQuotes (Quoted field unterminated). Papa still builds {"name":"Ada","note":"hello\" "} (the closing quote and the space stay in note). Convert treats any result.errors as fatal (errors.length > 0), so those Quotes errors are shown and the live page writes no JSON. A closed Ada,"hello" is {"name":"Ada","note":"hello"} and Convert writes the JSON. A space inside the quotes (Ada,"hello ") is valid RFC 4180 and Convert writes {"name":"Ada","note":"hello "}. A space after the close then a newline is extraSpaces and Convert writes {"name":"Ada","note":"hello"}. A space after the close then a comma ("Ada" ,hello) is also extraSpaces and Convert writes {"name":"Ada","note":"hello"}. A space before the opening quote (Ada, "hello") is not a quoted field (the quote is not adjacent to the delimiter) and Convert writes {"name":"Ada","note":" \"hello\""}. A tab after the close is also InvalidQuotes ({"name":"Ada","note":"hello\"\ "}). A quoted 30 then a trailing space stays the string "30\" " (not the number 30). A quoted empty cell plus a trailing space (Ada,"" plus a space) is the same pair of Quotes errors; Papa still builds {"name":"Ada","note":"\" "} (not null). This is not the unclosed MissingQuotes-only case (Ada,"hello with no close). The live converter does not ignore Quotes errors and has no quote-repair mode.
A doubled quote inside a CSV field becomes one JSON quote
The live converter calls Papa.parse with header: true. RFC 4180 §7 says a double-quote inside a quoted field must be escaped by another double quote (ABNF 2DQUOTE). name,note plus Ada,"hel""lo" is that pair: the "" inside the quotes is one quote, so Convert writes {"name":"Ada","note":"hel\"lo"} and there is no Quotes error. JSON.stringify writes the quote as \" (RFC 8259). The RFC example "aaa","b""bb","ccc" becomes {"a":"aaa","b":"b\"bb","c":"ccc"}. A quote at the start (Ada,"""hello") is {"name":"Ada","note":"\"hello"}. A quote at the end (Ada,"hello""") is {"name":"Ada","note":"hello\""}. A field that is only a quote (Ada,"""") is {"name":"Ada","note":"\""} (not null; a quoted empty Ada,"" is still null). A comma inside the escaped field (Ada,"hel"",lo") stays in note ({"name":"Ada","note":"hel\",lo"}). A doubled quote in a header (name,"no""te" plus Ada,hello) becomes the key no"te. A single inner quote without doubling (Ada,"hel"lo") is InvalidQuotes (Trailing quote on quoted field is malformed); Papa still builds {"name":"Ada","note":"hel\"lo"}, but the live page treats that Quotes error as fatal and writes no JSON. A backslash is not RFC 4180 (Ada,"hel\"lo") and is also InvalidQuotes. An unquoted inner quote (Ada,hel"lo) is accepted by Papa as {"name":"Ada","note":"hel\"lo"} and Convert writes that JSON (RFC 4180 §5 would forbid a quote in an unquoted field). An unquoted doubled pair (Ada,hel""lo) keeps both quotes ({"name":"Ada","note":"hel\"\"lo"}). An odd triple mid-field (Ada,"hel"""lo") is InvalidQuotes. A doubled quote inside a number-looking cell (Ada,"3""0") stays the string "3\"0" (not the number 30). This is not the trailing-space InvalidQuotes case (Ada,"hello" plus a space) and not the unclosed MissingQuotes-only case (Ada,"hello with no close). The live converter does not ignore Quotes errors and has no quote-repair mode.
Semicolon CSV converts; 3,14 stays a string
Papa leaves the delimiter blank, so guessDelimiter tries comma, tab, pipe, semicolon, and the ASCII record/unit separators, and keeps a candidate only when the average field count is greater than 1.99. name;age plus Ada;30 averages 2 fields, so semicolon wins. There is no UndetectableDelimiter, and Convert writes [{"name":"Ada","age":30}]. name|age plus Ada|30 is the same with pipe. Papa's FLOAT regex uses a decimal point, not a comma, so n;x plus 3,14;foo becomes {"n":"3,14","x":"foo"}. RFC 4180 quotes around "3,14" are stripped first, so a quoted 3,14 is still the string "3,14". Write 3.14 for a JSON number: n;x plus 3.14;foo becomes {"n":3.14,"x":"foo"}. A quoted "3.14" is still 3.14. A comma inside a quoted field in a semicolon table stays in the field (Ada;"hel,lo" is {"name":"Ada","note":"hel,lo"}). Mixed delimiters fail: name,age plus Ada;30 is UndetectableDelimiter plus TooFewFields ({"name":"Ada;30"}); name;age plus Ada,30 is UndetectableDelimiter plus TooManyFields. Convert treats those errors as fatal and writes no JSON. Unquoted 3,14 in a comma table is also TooManyFields. A one-column note then hello;world is still UndetectableDelimiter. A space after the semicolon (Ada; 30) becomes the number 30. zip;x plus 007;foo is still {"zip":7,"x":"foo"}. An empty cell after a semicolon (Ada;) is null. This is not the tab-padded one-column TooManyFields and not the one-column name / Ada UndetectableDelimiter. The live converter does not expose a delimiter picker or a decimal-comma locale toggle.
CSV True stays the JSON string True
Papa's boolean test is exact equality: value === 'true' or value === 'TRUE' becomes JSON true; value === 'false' or value === 'FALSE' becomes JSON false. Title-case True and False are not those four strings, so flag,True becomes {"flag":"True"} and False stays "False". RFC 4180 quotes around "true" are stripped first, so a quoted true is still the boolean true; a quoted True stays "True". yes and Yes stay strings. JSON has only lowercase true and false (RFC 8259 §3). The live converter always enables dynamicTyping; there is no keep-as-string toggle.
CSV 2026-09-18T12:00:00Z becomes JSON 2026-09-18T12:00:00.000Z
After the FLOAT and true / false tests, Papa's ISO_DATE test matches a full-string ISO-8601 timestamp: YYYY-MM-DD, T, hours and minutes, optional seconds and a fraction, then Z or a ±HH:MM offset. A match becomes new Date(value). Convert then pretty-prints that Date with JSON.stringify. Date.prototype.toJSON (ECMA-262) calls toISOString, which writes UTC with milliseconds, so when,2026-09-18T12:00:00Z becomes {"when":"2026-09-18T12:00:00.000Z"}. RFC 4180 quotes around "2026-09-18T12:00:00Z" are stripped first, so a quoted timestamp is still a Date. A date-only cell such as 2026-09-18 stays a string. A space instead of T stays a string. A T-time without an offset, such as 2026-09-18T12:00:00, stays a string. An offset such as 2026-09-18T12:00:00-05:00 stringifies as 2026-09-18T17:00:00.000Z. Minute-only 2026-09-18T12:00Z also matches and stringifies as 2026-09-18T12:00:00.000Z. A fraction already present, such as 2026-09-18T12:00:00.123Z, keeps .123Z. JSON has no Date type (RFC 8259). The live converter always enables dynamicTyping; there is no keep-as-string toggle.
An empty CSV cell becomes JSON null
After the FLOAT, true / false, and ISO_DATE tests, Papa's parseDynamic returns value === '' ? null : value. An empty cell is that empty string, so name,note plus Ada, becomes {"name":"Ada","note":null}. RFC 4180 quotes around "" are stripped first, so a quoted empty cell is still null. A space-only cell stays the string " ". The word null stays the string "null". N/A stays "N/A". skipEmptyLines drops a completely empty line, not an empty cell inside a kept row. A blank line between Ada,30 and Bob,40 is skipped; a spaces-only row is not. JSON.stringify writes the null token (RFC 8259 §3) and keeps the key. The live converter always enables dynamicTyping; there is no keep-as-empty-string toggle.
A blank CSV line converts; a spaces-only row does not
Papa's testEmptyLine for skipEmptyLines: true is s.length === 1 && s[0].length === 0— a single empty field. name,age plus Ada,30, a blank line, then Bob,40 drops that empty row. guessDelimiter also skips it, so the average field count stays 2. Convert writes [{"name":"Ada","age":30},{"name":"Bob","age":40}]. A CRLF blank line is the same. A quoted empty row "" is one empty field after the quotes are stripped, so it is also skipped. A comma-only row , is two empty fields (["",""]); testEmptyLine is false, so Convert writes {"name":null,"age":null} plus the Ada row. A spaces-only row (three spaces) is one non-empty field, so it is kept. That one-field row drops the average field count to 1.75 (below 1.99), so guessDelimiter reports UndetectableDelimiter and the row is TooFewFields (expected 2 fields but parsed 1). Papa still builds {"name":" "} between Ada and Bob, but Convert treats those errors as fatal and writes no JSON. A tab-only row is the same pair of errors. skipEmptyLines: 'greedy' would use s.join("").trim() === "" and would drop the spaces-only row and the comma-only row; the page does not enable greedy. A header-only name,age (no data rows) is [] with no errors — that is not skipEmptyLines. If the whole box is only whitespace, Convert shows “Please enter CSV data” and does not call Papa.parse. This is not the empty-cell null case (Ada, keeps the row).
A CSV # note,ignored row converts as data
Papa leaves comments at the default false. RFC 4180 has no comment syntax, so a line that starts with # is a data row. name,age plus # note,ignored plus Ada,30 has no error, and Convert writes [{"name":"# note","age":"ignored"},{"name":"Ada","age":30}]. A CRLF hash line is the same. #Ada,30 plus Bob,40 is [{"name":"#Ada","age":30},{"name":"Bob","age":40}]. A one-field # comment row (no comma) is UndetectableDelimiter plus TooFewFields; Papa still builds {"name":"# comment"}, but Convert treats those errors as fatal and writes no JSON. A quoted "# comment" is a field value ({"name":"Ada","note":"# comment"}). A space before the hash ( # note,30) is still a data row. // note,30 is also kept. comments: true (or comments: '#') would skip a line whose first characters are #; comments: '//' would skip // lines. A space or tab before # is still kept even then. The page does not set that option. A hash in the header (#name,age plus Ada,30) becomes the key #name. In a semicolon table, name;age plus # note;30 plus Ada;30 writes both rows. A hash after the delimiter (Ada,#30) is the string "#30", not a comment. This is not skipEmptyLines (a blank line is dropped; a # line is not).
CSV 1_000 stays the string 1_000
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. There is no underscore. n,1_000 becomes {"n":"1_000"}. RFC 4180 quotes around "1_000" are stripped first, so a quoted 1_000 is still the string "1_000". 1000 becomes the number 1000. 1_000.5 stays "1_000.5". -1_000 stays "-1_000". +1_000 stays "+1_000". 1_000_000 stays "1_000_000". 1_000e2 stays "1_000e2". Surrounding spaces are kept, so 1_000 stays " 1_000 " — the regex never matches, so parseFloat is not called. parseFloat of "1_000" would be 1 (it stops at the underscore); Papa never takes that path. JSON.parse of "1_000" throws (RFC 8259 §6 has no digit separator). Write 1000 for the number. In a semicolon table, n;x plus 1_000;foo is {"n":"1_000","x":"foo"}. Unquoted 1,000 in a comma table is TooManyFields. This is not the European 3,14 case. YAML 1.1 !!int ignored _; Core and the live converter do not.
CSV 1e stays the string 1e
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. The exponent group is [eE][-+]?\d+: e or E, an optional sign, and one or more digits. n,1e becomes {"n":"1e"}. RFC 4180 quotes around "1e" are stripped first, so a quoted 1e is still the string "1e". 1e+ stays "1e+". 1e- stays "1e-". 1E stays "1E". -1e stays "-1e". 1e+2 becomes the number 100. 1e2 and 1E2 are also 100. 1e-2 becomes 0.01. 1e+0 becomes 1. .5e stays ".5e"; 1.e stays "1.e" (.5e2 and 1.e2 are complete and become 50 and 100). 1e+2.5 stays "1e+2.5". e2 stays "e2". Surrounding spaces are kept, so 1e stays " 1e " — the regex never matches, so parseFloat is not called. parseFloat of "1e" or "1e+" would be 1 (they stop at the incomplete exponent); Papa never takes that path. JSON.parse of "1e" or "1e+" throws (RFC 8259 §6 exp needs one or more DIGIT). JSON.parse of "1e+2" is 100. Write 1e+2 or 100 for the number. In a semicolon table, n;x plus 1e;foo is {"n":"1e","x":"foo"}. A CRLF 1e is the same. This is not the plus-prefixed +1e2 case.
CSV 0x1 stays the string 0x1
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. There is no 0x or 0X hex prefix. n,0x1 becomes {"n":"0x1"}. RFC 4180 quotes around "0x1" are stripped first, so a quoted 0x1 is still the string "0x1". 0XFF stays "0XFF". 0xff stays "0xff". -0x1 stays "-0x1". +0x1 stays "+0x1". 0x stays "0x". 0x10 stays "0x10" (not 16). 0b10 stays "0b10". 0o10 stays "0o10". Surrounding spaces are kept, so 0x1 stays " 0x1 " — the regex never matches, so parseFloat is not called. parseFloat of "0x1" would be 0 (it stops at the x); Papa never takes that path. Number and parseInt of "0x1" are 1. JSON.parse of "0x1" throws (RFC 8259 §6 has no hex prefix). YAML 1.2.2 Core 0x [0-9a-fA-F]+ would turn 0x1 into 1; the live converter does not. Write 1 for the number (or 16 for 0x10). In a semicolon table, n;x plus 0x1;foo is {"n":"0x1","x":"foo"}. A CRLF 0x1 is the same. This is not the incomplete-exponent 1e case.
CSV 1e16 stays the string 1e16
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. 1e16 matches that pattern (e plus one or more digits). After the match, testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, where MAX_FLOAT is 2^53. parseFloat of "1e16" is 10000000000000000, which is not < MAX_FLOAT, so n,1e16 becomes {"n":"1e16"}. RFC 4180 quotes around "1e16" are stripped first, so a quoted 1e16 is still the string "1e16". 1e+16 stays "1e+16". 1E16 stays "1E16". -1e16 stays "-1e16". 1.0e16 stays "1.0e16". 10e15 stays "10e15". 1e15 becomes the number 1000000000000000. 9e15 becomes 9000000000000000. 1e+2 still becomes 100. If the exclusive-range check fails, the original cell is kept, so 1e16 stays " 1e16 ". JSON.parse of "1e16" is the number 10000000000000000 (RFC 8259 §6 allows exp). Write 1e15 for the number. In a semicolon table, n;x plus 1e16;foo is {"n":"1e16","x":"foo"}. A CRLF 1e16 is the same. This is not the incomplete-exponent 1e case and not the integer 9007199254740992 case.
CSV 1e-324 becomes JSON 0
Papa's FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. 1e-324 matches that pattern (e, a minus, and one or more digits). After the match, testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT. parseFloat of "1e-324" is 0 (IEEE-754 underflow; Number.MIN_VALUE is 5e-324). 0 is strictly inside ±2^53, so n,1e-324 becomes {"n":0}. RFC 4180 quotes around "1e-324" are stripped first, so a quoted 1e-324 is still 0. 1e-325 is also 0. 2e-324 is 0. 5e-324 becomes 5e-324. 4e-324 becomes 5e-324. 1e-323 becomes 1e-323. 1e-308 becomes 1e-308. 1E-324 and 1.0e-324 are also 0. 10e-325 is 0. -1e-324 is JavaScript -0; JSON.stringify writes 0. 1e-324 becomes 0. +1e-324 stays "+1e-324". JSON.parse of "1e-324" is also 0 (RFC 8259 §6 allows exp). Write 5e-324 for Number.MIN_VALUE. In a semicolon table, n;x plus 1e-324;foo is {"n":0,"x":"foo"}. A CRLF 1e-324 is the same. This is not the complete-exponent 1e16 case and not the incomplete-exponent 1e case.
Delimiters and quoting
Papa is left to auto-detect the delimiter (comma, tab, pipe, semicolon, or ASCII record/unit separators) and uses RFC 4180-style quoting. There is no delimiter dropdown and no “treat every line as one field” mode. Quoted commas inside a cell stay inside that field.
What this mapping is not
It is not a typed schema, not a database import, and not a Mongo/Firebase loader. After Convert you have a JSON array you can copy or download. Developer limits — Excel, reverse conversion, and parse errors — are in CSV to JSON limits for developers. If you need to inspect the JSON again, use the JSON formatter.
Transform a CSV table
Header row becomes keys. Empty lines drop. Numbers and true/false type. No upload.
Use CSV to JSON →FAQ
- Is the output an array of arrays?
- No. Papa.parse runs with header: true, so the first row is field names and each later row is an object keyed by those names. The page then JSON.stringifies that array with 2-space indentation. Duplicate headers are renamed so object properties are not overwritten: the first name stays name; a later name becomes name_1. name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"}. A short data row such as name,age,city plus Ada,30 is TooFewFields; Papa still builds {"name":"Ada","age":30}, but the live page treats that FieldMismatch as fatal and writes no JSON. An unclosed quote such as name,note plus Ada,"hello is MissingQuotes; Papa still builds {"name":"Ada","note":"hello"}, but the live page treats that Quotes error as fatal and writes no JSON. A trailing space after a closing quote such as Ada,"hello" plus a space is InvalidQuotes plus MissingQuotes; Papa still builds {"name":"Ada","note":"hello\" "}, but the live page treats those Quotes errors as fatal and writes no JSON. A doubled quote inside a quoted field such as Ada,"hel""lo" is RFC 4180 §7 (2DQUOTE); Convert writes {"name":"Ada","note":"hel\"lo"} and there is no Quotes error. A two-column semicolon table such as name;age plus Ada;30 is auto-detected (average field count 2); Convert writes [{"name":"Ada","age":30}]. Papa’s FLOAT test uses a decimal point, so n;x plus 3,14;foo stays {"n":"3,14","x":"foo"}. A two-column hash line such as # note,ignored is kept as data (comments defaults to false); Convert writes {"name":"# note","age":"ignored"} plus the Ada row. A one-field # comment row is UndetectableDelimiter plus TooFewFields. An incomplete exponent such as 1e or 1e+ stays the string "1e" or "1e+" (FLOAT needs one or more digits after e or E). Write 1e+2 for the number 100. A hexadecimal prefix such as 0x1 stays the string "0x1" (FLOAT has no 0x). Write 1 for the number. A complete exponent such as 1e16 stays the string "1e16" (parseFloat is 1e16, which is not < 2^53). Write 1e15 for the number 1000000000000000. A complete tiny exponent such as 1e-324 becomes 0 (parseFloat underflows; 0 is inside ±2^53). Write 5e-324 for Number.MIN_VALUE. This is not an array-of-arrays dump.
- What do skipEmptyLines and dynamicTyping change?
- skipEmptyLines: true drops a single empty field (testEmptyLine is s.length === 1 && s[0].length === 0), not whitespace-only rows (that would need greedy) and not empty cells inside a kept row. A blank line between name,age / Ada,30 and Bob,40 is skipped and Convert writes [{"name":"Ada","age":30},{"name":"Bob","age":40}]. A spaces-only row is kept, drops the average field count below 1.99, and is UndetectableDelimiter plus TooFewFields. A comma-only row , is two empty fields ({"name":null,"age":null}) and Convert succeeds. A hash line is not empty, so it is not skipped. comments defaults to false (RFC 4180 has no comment syntax): name,age plus # note,ignored plus Ada,30 writes both rows. A one-field # comment row is UndetectableDelimiter plus TooFewFields. dynamicTyping: true turns decimal-literal numbers strictly inside ±2^53 and the strings true/false into number/boolean; an empty cell becomes JSON null; other cells stay strings. A padded ID such as 007 is a decimal literal, so it becomes 7. A plus-prefixed cell such as +30 stays a string. An underscore separator such as 1_000 stays the string "1_000" (FLOAT has no _). Write 1000 for the number. An incomplete exponent such as 1e or 1e+ stays the string "1e" or "1e+" (FLOAT needs one or more digits after e or E). Write 1e+2 for the number 100. A hexadecimal prefix such as 0x1 stays the string "0x1" (FLOAT has no 0x). Write 1 for the number. A complete exponent such as 1e16 stays the string "1e16" (parseFloat is 1e16, which is not < 2^53). Write 1e15 for the number 1000000000000000. A complete tiny exponent such as 1e-324 becomes 0 (parseFloat underflows; 0 is inside ±2^53). Write 5e-324 for Number.MIN_VALUE. A leading or trailing decimal such as .5 or 1. becomes a number. The range check is exclusive (floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, MAX_FLOAT = 2^53), so 9007199254740992 stays a string. Surrounding whitespace on a FLOAT cell is allowed (/^\s*…\s*$/), so 30 becomes 30. An unquoted both-side tab around 30 on a one-column header is auto-detected as a delimiter (TooManyFields; the live page writes no JSON). A quoted tab-padded 30 stays one cell and becomes 30 because \s includes tab. A clean one-column table such as name / Ada is UndetectableDelimiter (average field count is 1, below 1.99); Papa still builds [{"name":"Ada"}], but the live page treats that warning as fatal and writes no JSON. Duplicate headers are renamed so keys are not overwritten: name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"} and Convert writes that JSON. A short data row such as name,age,city plus Ada,30 is TooFewFields (expected 3 fields but parsed 2); Papa still builds {"name":"Ada","age":30} and omits city, but the live page writes no JSON. A trailing comma (Ada,30,) is three fields ({"name":"Ada","age":30,"city":null}) and Convert succeeds. A long row such as name,age plus Ada,30,extra is TooManyFields; Papa stores the extra cell on __parsed_extra. An unclosed quote such as name,note plus Ada,"hello is MissingQuotes ("Quoted field unterminated"); Papa still builds {"name":"Ada","note":"hello"}, but the live page writes no JSON. A closed Ada,"hello" succeeds. A trailing space after a closing quote such as Ada,"hello" plus a space is InvalidQuotes ("Trailing quote on quoted field is malformed") plus MissingQuotes; Papa still builds {"name":"Ada","note":"hello\" "}, but the live page writes no JSON. A doubled quote inside a quoted field such as Ada,"hel""lo" is RFC 4180 §7 (2DQUOTE); Convert writes {"name":"Ada","note":"hel\"lo"} and there is no Quotes error. A two-column semicolon table such as name;age plus Ada;30 is auto-detected (average field count 2); Convert writes [{"name":"Ada","age":30}]. Papa’s FLOAT test uses a decimal point, so n;x plus 3,14;foo stays {"n":"3,14","x":"foo"}. Write 3.14 for the number 3.14. Mixed comma and semicolon (name,age plus Ada;30) is UndetectableDelimiter; the live page writes no JSON.
- Why does CSV 007 become JSON 7 instead of the string 007?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT test accepts an optional minus, digits (leading zeros included), an optional decimal, and an optional exponent, then parseFloat returns a JavaScript Number when that value is inside ±(2^53). parseFloat("007") is 7, so zip,007 becomes {"zip":7}. 00123 becomes 123. 00 becomes 0. RFC 4180 quotes around "007" are stripped before that test, so a quoted 007 is still 7. 007a stays the string "007a". A date-only cell such as 2026-09-18 stays a string. European comma decimals stay strings: in a semicolon table, n;x plus 3,14;foo becomes {"n":"3,14","x":"foo"}. Write 3.14 for the number 3.14. The page always enables dynamicTyping; there is no keep-as-string toggle. JSON has no leading-zero integer (RFC 8259 §6). Nothing is uploaded.
- Why does CSV +30 stay the string +30 instead of JSON 30?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT test accepts an optional minus, digits (leading zeros included), an optional decimal, and an optional exponent — not a leading plus — then parseFloat returns a JavaScript Number when that value is inside ±(2^53). +30 is not that pattern, so n,+30 becomes {"n":"+30"}. RFC 4180 quotes around "+30" are stripped first, so a quoted +30 is still the string "+30". -30 becomes the number -30. 30 becomes 30. +30.5 and +007 stay strings. Plus is legal in an exponent, so 1e+2 becomes 100; +1e2 stays "+1e2". JSON has no plus-prefixed number (RFC 8259 §6); JSON.parse("+30") throws. The page always enables dynamicTyping; there is no coerce-plus toggle. Nothing is uploaded.
- Why does CSV 1_000 stay the string 1_000 instead of JSON 1000?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. There is no underscore. 1_000 is not that pattern, so n,1_000 becomes {"n":"1_000"}. RFC 4180 quotes around "1_000" are stripped first, so a quoted 1_000 is still the string "1_000". 1000 becomes the number 1000. 1_000.5 stays "1_000.5". -1_000 stays "-1_000". +1_000 stays "+1_000". 1_000_000 stays "1_000_000". 1_000e2 stays "1_000e2". Surrounding spaces are kept, so 1_000 stays " 1_000 " — the regex never matches, so parseFloat is not called. parseFloat("1_000") would be 1 (it stops at the underscore); Papa never takes that path. JSON.parse("1_000") throws (RFC 8259 §6 has no digit separator). Write 1000 for the number. In a semicolon table, n;x plus 1_000;foo is {"n":"1_000","x":"foo"}. Unquoted 1,000 in a comma table is TooManyFields (1 and 000 are two fields). This is not the European 3,14 case. YAML 1.1 !!int ignored _; Core and this page do not. The page always enables dynamicTyping; there is no underscore-strip toggle. Nothing is uploaded.
- Why does CSV 1e stay the string 1e instead of JSON 1 or 100?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. The exponent group is [eE][-+]?\d+: e or E, an optional sign, and one or more digits. 1e has the letter and no digits, so n,1e becomes {"n":"1e"}. RFC 4180 quotes around "1e" are stripped first, so a quoted 1e is still the string "1e". 1e+ stays "1e+". 1e- stays "1e-". 1E stays "1E". -1e stays "-1e". 1e+2 becomes the number 100. 1e2 and 1E2 are also 100. 1e-2 becomes 0.01. 1e+0 becomes 1. .5e stays ".5e". 1.e stays "1.e" (.5e2 and 1.e2 are complete and become 50 and 100). 1e+2.5 stays "1e+2.5" (a fraction after the exponent digits is not in the pattern). e2 stays "e2". Surrounding spaces are kept, so 1e stays " 1e " — the regex never matches, so parseFloat is not called. parseFloat("1e") and parseFloat("1e+") would be 1 (they stop at the incomplete exponent); Papa never takes that path. JSON.parse("1e") and JSON.parse("1e+") throw (RFC 8259 §6 exp needs one or more DIGIT). JSON.parse("1e+2") is 100. Write 1e+2 or 100 for the number. In a semicolon table, n;x plus 1e;foo is {"n":"1e","x":"foo"}. A CRLF 1e is the same. This is not the plus-prefixed +1e2 case (that leading plus is outside the optional-minus FLOAT). The page always enables dynamicTyping; there is no coerce-exponent toggle. Nothing is uploaded.
- Why does CSV 0x1 stay the string 0x1 instead of JSON 1?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. There is no 0x or 0X hex prefix. 0x1 is not that pattern, so n,0x1 becomes {"n":"0x1"}. RFC 4180 quotes around "0x1" are stripped first, so a quoted 0x1 is still the string "0x1". 0XFF stays "0XFF". 0xff stays "0xff". -0x1 stays "-0x1". +0x1 stays "+0x1". 0x stays "0x". 0x10 stays "0x10" (not 16). 0b10 stays "0b10". 0o10 stays "0o10". Surrounding spaces are kept, so 0x1 stays " 0x1 " — the regex never matches, so parseFloat is not called. parseFloat("0x1") would be 0 (it stops at the x); Papa never takes that path. Number("0x1") and parseInt("0x1") are 1. JSON.parse("0x1") throws (RFC 8259 §6 has no hex prefix; parse accepts 0 then hits x). YAML 1.2.2 Core 0x [0-9a-fA-F]+ would turn 0x1 into 1; the live converter does not. Write 1 for the number (or 16 for 0x10). In a semicolon table, n;x plus 0x1;foo is {"n":"0x1","x":"foo"}. A CRLF 0x1 is the same. This is not the incomplete-exponent 1e case and not the underscore 1_000 case. The page always enables dynamicTyping; there is no hex-coerce toggle. Nothing is uploaded.
- Why does CSV 1e16 stay the string 1e16 instead of JSON 10000000000000000?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. 1e16 matches that pattern (e plus one or more digits). After the match, testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, where MAX_FLOAT is 2^53 (9007199254740992). parseFloat("1e16") is 10000000000000000, which is not < MAX_FLOAT, so n,1e16 becomes {"n":"1e16"}. RFC 4180 quotes around "1e16" are stripped first, so a quoted 1e16 is still the string "1e16". 1e+16 stays "1e+16". 1E16 stays "1E16". -1e16 stays "-1e16". 1.0e16 stays "1.0e16". 10e15 stays "10e15" (parseFloat is also 1e16). 1e15 becomes the number 1000000000000000 (1e15 is < 2^53). 9e15 becomes 9000000000000000. 1e+2 still becomes 100. If the exclusive-range check fails, the original cell is kept, so 1e16 stays " 1e16 " — the regex matched and parseFloat ran, but 1e16 is not < MAX_FLOAT. +1e16 stays "+1e16" because of the leading plus (optional-minus FLOAT), not this range check. JSON.parse("1e16") is the number 10000000000000000 (exactly representable; RFC 8259 §6 allows exp). This is not the incomplete-exponent 1e case (that regex never matches), not the integer 9007199254740992 case (same range check, different spelling), and not the underflow 1e-324 case (that parseFloat is 0, which is inside ±2^53). Write 1e15 for the number 1000000000000000, or 9007199254740991 for the largest typed integer. In a semicolon table, n;x plus 1e16;foo is {"n":"1e16","x":"foo"}. A CRLF 1e16 is the same. The page always enables dynamicTyping; there is no BigInt path. Nothing is uploaded.
- Why does CSV 1e-324 become JSON 0 instead of a tiny number or the string 1e-324?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — an optional minus, digits, an optional decimal, and an optional exponent. 1e-324 matches that pattern (e, a minus, and one or more digits). After the match, testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, where MAX_FLOAT is 2^53 (9007199254740992). parseFloat("1e-324") is 0 (IEEE-754 underflow; Number.MIN_VALUE is 5e-324). 0 is strictly inside ±2^53, so n,1e-324 becomes {"n":0}. RFC 4180 quotes around "1e-324" are stripped first, so a quoted 1e-324 is still 0. 1e-325 is also 0. 2e-324 is 0 (rounds toward zero). 5e-324 becomes 5e-324. 4e-324 becomes 5e-324 (rounds to MIN_VALUE). 1e-323 becomes 1e-323. 1e-308 becomes 1e-308. 1E-324 and 1.0e-324 are also 0. 10e-325 is 0 (parseFloat is also 0). -1e-324 is JavaScript -0; JSON.stringify writes 0 (JSON has no signed zero). Surrounding spaces still type, so 1e-324 becomes 0 — the regex matched, parseFloat ran, and 0 is inside the range. +1e-324 stays "+1e-324" because of the leading plus (optional-minus FLOAT), not this underflow. JSON.parse("1e-324") is also 0 (RFC 8259 §6 allows exp). This is not the complete-exponent 1e16 case (that parseFloat is 1e16, which is not < MAX_FLOAT, so the original string is kept) and not the incomplete-exponent 1e case (that regex never matches). Write 5e-324 for Number.MIN_VALUE. In a semicolon table, n;x plus 1e-324;foo is {"n":0,"x":"foo"}. A CRLF 1e-324 is the same. The page always enables dynamicTyping; there is no keep-as-string toggle. Nothing is uploaded.
- Why do CSV .5 and 1. become JSON 0.5 and 1?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT test accepts an optional minus, then a leading decimal such as .5, a trailing decimal such as 1., or a complete decimal such as 0.5, plus an optional exponent, then parseFloat returns a JavaScript Number when that value is inside ±(2^53). parseFloat(".5") is 0.5, so n,.5 becomes {"n":0.5}. parseFloat("1.") is 1, so n,1. becomes {"n":1}. RFC 4180 quotes around ".5" are stripped first, so a quoted .5 is still 0.5. A quoted 1. is still 1. -.5 becomes the number -0.5. -1. becomes -1. 0.5 stays 0.5. 1.0 becomes 1. A lone . stays the string ".". 1.2.3 stays "1.2.3". .5e2 becomes 50. 1.e2 becomes 100. JSON.parse(".5") and JSON.parse("1.") throw (RFC 8259 §6 requires an integer part and at least one digit after a decimal point). Write 0.5 or 1.0 in JSON. The page always enables dynamicTyping; there is no keep-as-string toggle. Nothing is uploaded.
- Why does CSV 9007199254740992 stay the string 9007199254740992 instead of a JSON number?
- The live converter calls Papa.parse with dynamicTyping: true. After the FLOAT regex matches, Papa’s testFloat calls parseFloat and keeps a Number only when floatValue > MIN_FLOAT && floatValue < MAX_FLOAT, where MAX_FLOAT is 2^53 (9007199254740992). Those bounds are exclusive, so 9007199254740991 becomes the number 9007199254740991 and n,9007199254740992 becomes {"n":"9007199254740992"}. RFC 4180 quotes around "9007199254740992" are stripped first, so a quoted 9007199254740992 is still the string "9007199254740992". -9007199254740991 becomes the number -9007199254740991. -9007199254740992 stays the string "-9007199254740992". 9007199254740993 stays the string "9007199254740993" because parseFloat of that digit string is 9007199254740992, which is not < MAX_FLOAT. Number.MAX_SAFE_INTEGER is 2^53 − 1. JSON.parse("9007199254740992") is the number 9007199254740992 (exactly representable); JSON.parse("9007199254740993") is 9007199254740992 (IEEE-754). A complete exponent such as 1e16 is the same exclusive check: parseFloat("1e16") is 10000000000000000, so n,1e16 becomes {"n":"1e16"}. 1e15 becomes 1000000000000000. The page always enables dynamicTyping; there is no BigInt path. Nothing is uploaded.
- Why does CSV 30 (spaces around 30) become JSON 30?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/, so surrounding whitespace is allowed. After that match, testFloat calls parseFloat and keeps a Number when that value is strictly inside ±2^53. parseFloat(" 30 ") is 30, so n, 30 becomes {"n":30}. RFC 4180 quotes around " 30 " are stripped first, so a quoted 30 is still 30. A leading-only space ( 30) and a trailing-only space (30 ) are also 30. -30 becomes -30. .5 becomes 0.5. 1. becomes 1. 1e+2 becomes 100. An internal space such as 3 0 stays the string "3 0". A space-only cell stays the string " ". Papa’s boolean test is exact equality, so true stays the string " true ". Papa’s ISO_DATE test is a full-string match, so 2026-09-18T12:00:00Z stays a string. If the exclusive-range check fails, the original cell is kept, so 9007199254740992 stays " 9007199254740992 ". 9007199254740991 becomes 9007199254740991. JSON.parse(" 30 ") is also 30 (RFC 8259 insignificant whitespace). The page always enables dynamicTyping; there is no trim toggle. Nothing is uploaded.
- Why does an unquoted tab-padded CSV 30 error as TooManyFields, while a quoted tab-padded 30 becomes JSON 30?
- The live converter calls Papa.parse with dynamicTyping: true and a blank delimiter, so Papa auto-detects from comma, tab, pipe, semicolon, or the ASCII record/unit separators. guessDelimiter keeps a candidate only when the average field count is greater than 1.99. An unquoted both-side tab around 30 on a one-column header (n, then a tab, 30, and a tab) is three fields, so tab wins and the row is TooManyFields (expected 1 fields but parsed 3). The page shows that message and writes no JSON. RFC 4180 quotes keep those tabs inside one cell, so tab is not guessed. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/, and \s includes tab, so parseFloat of a quoted tab-padded 30 is 30. In a two-column comma table, n,x plus a quoted tab-padded 30 and foo becomes {"n":30,"x":"foo"}. The same two-column table with an unquoted tab-padded 30 is also 30 because comma already won. A leading-only tab (tab then 30) or a trailing-only tab (30 then a tab) on a one-column header averages 1.5 fields, so tab is not chosen; Papa still types 30, but UndetectableDelimiter defaults to comma and the page still writes no JSON. A quoted tab-padded true stays a string (boolean is exact equality). A quoted tab-padded ISO timestamp stays a string (ISO_DATE is a full-string match). If the exclusive-range check fails, the original cell is kept, so a quoted tab-padded 9007199254740992 stays that tab-padded string. JSON.parse of a tab-padded 30 is also 30 (RFC 8259 insignificant whitespace). The page always enables dynamicTyping; there is no delimiter picker. Nothing is uploaded.
- Why does a one-column CSV such as name / Ada error as UndetectableDelimiter and write no JSON?
- The live converter calls Papa.parse with a blank delimiter, so guessDelimiter tries comma, tab, pipe, semicolon, or the ASCII record/unit separators, and keeps a candidate only when the average field count is greater than 1.99. A one-column table (name, then Ada) averages 1 field, so no delimiter wins. Papa still defaults to comma and result.data is [{"name":"Ada"}]. Convert treats any result.errors as fatal (errors.length > 0), so UndetectableDelimiter ("Unable to auto-detect delimiting character; defaulted to ','") is shown and the page writes no JSON. zip then 007 is the same warning; Papa would type [{"zip":7}] but the page still writes no JSON. Quoted "name" / "Ada" is the same. Two-column n,x plus 30,foo averages 2 fields, comma wins, there is no warning, and Convert writes [{"n":30,"x":"foo"}]. Adding a dummy second column (name,x then Ada,) becomes [{"name":"Ada","x":null}] because the empty cell is null, and Convert succeeds. This is not TooManyFields (that is an unquoted both-side tab around 30 on a one-column header). A leading-only tab or trailing-only tab on one column is also UndetectableDelimiter. A two-column semicolon table (name;age / Ada;30) is the opposite: semicolon wins and Convert writes JSON. The page does not ignore delimiter warnings and has no delimiter picker. Nothing is uploaded.
- Why does CSV True stay the string True instead of JSON true?
- The live converter calls Papa.parse with dynamicTyping: true. Papa’s boolean test is exact equality: value === "true" or value === "TRUE" becomes JSON true; value === "false" or value === "FALSE" becomes JSON false. Title-case True and False are not those four strings, so flag,True becomes {"flag":"True"} and False stays "False". RFC 4180 quotes around "true" are stripped first, so a quoted true is still the boolean true; a quoted True stays "True". yes and Yes stay strings. JSON has only lowercase true and false (RFC 8259 §3). The page always enables dynamicTyping; there is no keep-as-string toggle. Nothing is uploaded.
- Why does CSV 2026-09-18T12:00:00Z become JSON 2026-09-18T12:00:00.000Z?
- The live converter calls Papa.parse with dynamicTyping: true. After the FLOAT and true/false tests, Papa’s ISO_DATE test matches a full-string ISO-8601 timestamp: YYYY-MM-DD, T, hours and minutes, optional seconds and a fraction, then Z or a ±HH:MM offset. A match becomes new Date(value). Convert then JSON.stringifies result.data. Date.prototype.toJSON (ECMA-262) calls toISOString, which writes UTC with milliseconds, so when,2026-09-18T12:00:00Z becomes {"when":"2026-09-18T12:00:00.000Z"}. RFC 4180 quotes around "2026-09-18T12:00:00Z" are stripped first, so a quoted timestamp is still a Date. A date-only cell such as 2026-09-18 stays a string. A space instead of T stays a string. A T-time without an offset, such as 2026-09-18T12:00:00, stays a string. An offset such as 2026-09-18T12:00:00-05:00 stringifies as 2026-09-18T17:00:00.000Z. JSON has no Date type (RFC 8259). The page always enables dynamicTyping; there is no keep-as-string toggle. Nothing is uploaded.
- Why does an empty CSV cell become JSON null instead of an empty string?
- The live converter calls Papa.parse with dynamicTyping: true. After the FLOAT, true/false, and ISO_DATE tests, Papa’s parseDynamic returns value === "" ? null : value. An empty cell is that empty string, so name,note plus Ada, becomes {"name":"Ada","note":null}. RFC 4180 quotes around "" are stripped first, so a quoted empty cell is still null. A space-only cell stays the string " ". The word null stays the string "null". N/A stays "N/A". skipEmptyLines: true drops a completely empty line, not an empty cell inside a kept row. A blank line between Ada,30 and Bob,40 is skipped and Convert writes both rows; a spaces-only row is not skipped (UndetectableDelimiter plus TooFewFields). JSON.stringify writes the null token (RFC 8259 §3) and keeps the key. The page always enables dynamicTyping; there is no keep-as-empty-string toggle. Nothing is uploaded.
- Why does a blank CSV line between rows convert, while a spaces-only row errors as UndetectableDelimiter?
- The live converter calls Papa.parse with skipEmptyLines: true. Papa’s testEmptyLine for that boolean is s.length === 1 && s[0].length === 0 — a single empty field. A completely empty line between name,age / Ada,30 and Bob,40 is that row, so it is dropped. guessDelimiter also skips it, so the average field count stays 2. There is no UndetectableDelimiter, and Convert writes [{"name":"Ada","age":30},{"name":"Bob","age":40}]. A CRLF blank line is the same. A quoted empty row "" is one empty field after the quotes are stripped, so it is also skipped. A comma-only row , is two empty fields (["",""]); testEmptyLine is false, so Convert writes {"name":null,"age":null} plus the Ada row. A spaces-only row (three spaces) is one non-empty field, so it is kept. That one-field row drops the average field count to 1.75 (below 1.99), so guessDelimiter reports UndetectableDelimiter and the row is TooFewFields (expected 2 fields but parsed 1). Papa still builds {"name":" "} between Ada and Bob, but Convert treats those errors as fatal and writes no JSON. A tab-only row is the same pair of errors. skipEmptyLines: "greedy" would use s.join("").trim() === "" and would drop the spaces-only row and the comma-only row; the page does not enable greedy. A header-only name,age (no data rows) is [] with no errors — that is not skipEmptyLines. If the whole box is only whitespace, Convert shows “Please enter CSV data” and does not call Papa.parse. This is not the empty-cell null case (Ada, keeps the row). A hash line is not empty, so it is not skipped (comments defaults to false). The page does not expose a greedy toggle. Nothing is uploaded.
- Why does a CSV # note,ignored row convert as data instead of being skipped?
- The live converter leaves comments at the default false. RFC 4180 has no comment syntax. A line that starts with # is a data row. name,age plus # note,ignored plus Ada,30 has no error, and Convert writes [{"name":"# note","age":"ignored"},{"name":"Ada","age":30}]. A CRLF hash line is the same. #Ada,30 plus Bob,40 is [{"name":"#Ada","age":30},{"name":"Bob","age":40}]. A one-field # comment row (no comma) is UndetectableDelimiter plus TooFewFields (the average field count drops below 1.99); Papa still builds {"name":"# comment"}, but Convert treats those errors as fatal and writes no JSON. A quoted "# comment" is a field value: name,note plus Ada,"# comment" becomes {"name":"Ada","note":"# comment"}. A space before the hash ( # note,30) is still a data row ({"name":" # note","age":30}). // note,30 is also kept. comments: true (or comments: "#") would skip a line whose first characters are #; comments: "//" would skip // lines. A space or tab before # is still kept even then. The page does not set that option. A hash in the header (#name,age plus Ada,30) becomes the key #name. In a semicolon table, name;age plus # note;30 plus Ada;30 writes both rows. A hash after the delimiter (Ada,#30) is the string "#30", not a comment. This is not skipEmptyLines (a blank line is dropped; a # line is not). The page does not expose a comments picker. Nothing is uploaded.
- Why does a semicolon CSV such as name;age / Ada;30 convert, while 3,14 stays the string 3,14?
- The live converter calls Papa.parse with a blank delimiter, so guessDelimiter tries comma, tab, pipe, semicolon, or the ASCII record/unit separators, and keeps a candidate only when the average field count is greater than 1.99. name;age plus Ada;30 averages 2 fields, so semicolon wins. There is no UndetectableDelimiter, and Convert writes [{"name":"Ada","age":30}]. name|age plus Ada|30 is the same with pipe. Papa’s FLOAT regex is /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/ — a decimal point, not a comma — so n;x plus 3,14;foo becomes {"n":"3,14","x":"foo"}. RFC 4180 quotes around "3,14" are stripped first, so a quoted 3,14 is still the string "3,14". Write 3.14 for a JSON number: n;x plus 3.14;foo becomes {"n":3.14,"x":"foo"}. A quoted "3.14" is still 3.14. A comma inside a quoted field in a semicolon table stays in the field (Ada;"hel,lo" is {"name":"Ada","note":"hel,lo"}). Mixed delimiters fail: name,age plus Ada;30 is UndetectableDelimiter plus TooFewFields ({"name":"Ada;30"}); name;age plus Ada,30 is UndetectableDelimiter plus TooManyFields. Convert treats those errors as fatal and writes no JSON. Unquoted 3,14 in a comma table is also TooManyFields (3 and 14 are two fields). A one-column note then hello;world keeps the semicolon inside one cell and is still UndetectableDelimiter. A space after the semicolon (Ada; 30) becomes the number 30 because FLOAT allows surrounding whitespace. zip;x plus 007;foo is still {"zip":7,"x":"foo"}. An empty cell after a semicolon (Ada;) is null. This is not the tab-padded one-column TooManyFields and not the one-column name / Ada UndetectableDelimiter. The page does not expose a delimiter picker or a decimal-comma locale toggle. Nothing is uploaded.
- Why does a duplicate CSV header name become JSON name_1?
- The live converter calls Papa.parse with header: true. Duplicate field names are renamed so later columns do not overwrite earlier ones. The first name stays name; a later name becomes name_1, then name_2. name,name plus Ada,Bob becomes {"name":"Ada","name_1":"Bob"}. RFC 4180 quotes around "name" are stripped first, so a quoted "name","name" is the same. A third name becomes name_2 ({"name":"Ada","name_1":"Bob","name_2":"Cam"}). If name_1 already exists as a real header (name,name_1,name), the later name becomes name_2. name and Name are different keys (case-sensitive). A leading space (name, name) is a different key. Two empty headers become "" and _1. meta.renamedHeaders is { name_1: "name" }, but the page does not expose that map or transformHeader. JSON.parse of {"name":"Ada","name":"Bob"} keeps the last value (RFC 8259 §4); Papa avoids that overwrite. There is no FieldMismatch; Convert writes the JSON. Nothing is uploaded.
- Why does a short CSV row such as name,age,city / Ada,30 error as TooFewFields and write no JSON?
- The live converter calls Papa.parse with header: true. After each data row is built, Papa compares the parsed field count with the header count. name,age,city plus Ada,30 is TooFewFields ("Too few fields: expected 3 fields but parsed 2"). Papa still builds {"name":"Ada","age":30} and omits city (the missing key is not null). Convert treats any result.errors as fatal (errors.length > 0), so that FieldMismatch is shown and the page writes no JSON. A one-cell row Ada is TooFewFields expected 3 parsed 1 ({"name":"Ada"}). RFC 4180 quotes around "Ada" do not change the count. A trailing comma (Ada,30,) is three fields: the empty city becomes JSON null and Convert writes {"name":"Ada","age":30,"city":null}. An empty middle cell (Ada,,NY) is also three fields ({"name":"Ada","age":null,"city":"NY"}). A long row such as name,age plus Ada,30,extra is TooManyFields ("Too many fields: expected 2 fields but parsed 3"); Papa stores the extra cell on __parsed_extra as ["extra"]. Two extras become ["extra","more"]. A quoted extra is the same. This is not the tab-padded one-column TooManyFields (that is delimiter detect). The page does not ignore FieldMismatch and has no partial-output mode. Nothing is uploaded.
- Why does an unclosed CSV quote such as name,note / Ada,"hello error as MissingQuotes and write no JSON?
- The live converter calls Papa.parse with header: true. A field that starts with a quote must find a closing quote before EOF (RFC 4180). name,note plus Ada,"hello is MissingQuotes ("Quoted field unterminated"). Papa still builds {"name":"Ada","note":"hello"} (the opening quote is skipped; the rest of the input stays in note). Convert treats any result.errors as fatal (errors.length > 0), so that Quotes error is shown and the page writes no JSON. A closed Ada,"hello" is the same object and Convert writes the JSON. A comma inside the unclosed quote (Ada,"hello,world) stays in note ({"name":"Ada","note":"hello,world"}). A closed multiline quote (Ada,"hello then a newline then world") is valid RFC 4180 and Convert writes {"name":"Ada","note":"hello\nworld"}. An unclosed quote that continues onto the next line (Ada,"hello then Bob,ok) swallows that line into note ({"name":"Ada","note":"hello\nBob,ok"}) and is still MissingQuotes. This is not FieldMismatch (that is TooFewFields / TooManyFields). The page does not ignore Quotes errors and has no partial-output mode. Nothing is uploaded.
- Why does a trailing space after a closing CSV quote such as name,note / Ada,"hello" plus a space error as InvalidQuotes and write no JSON?
- The live converter calls Papa.parse with header: true. After a closing quote, Papa accepts a delimiter, a newline, or spaces then a delimiter or newline (extraSpaces). A trailing space at EOF after Ada,"hello" is not those. name,note plus Ada,"hello" and a trailing space is InvalidQuotes ("Trailing quote on quoted field is malformed") plus MissingQuotes ("Quoted field unterminated"). Papa still builds {"name":"Ada","note":"hello\" "} (the closing quote and the space stay in note). Convert treats any result.errors as fatal (errors.length > 0), so those Quotes errors are shown and the page writes no JSON. A closed Ada,"hello" is {"name":"Ada","note":"hello"} and Convert writes the JSON. A space inside the quotes (Ada,"hello ") is valid RFC 4180 and Convert writes {"name":"Ada","note":"hello "}. A space after the close then a newline is extraSpaces and Convert writes {"name":"Ada","note":"hello"}. A space after the close then a comma ("Ada" ,hello) is also extraSpaces and Convert writes {"name":"Ada","note":"hello"}. A space before the opening quote (Ada, "hello") is not a quoted field (the quote is not adjacent to the delimiter) and Convert writes {"name":"Ada","note":" \"hello\""}. A tab after the close is also InvalidQuotes ({"name":"Ada","note":"hello\"\t"}). A quoted 30 then a trailing space stays the string "30\" " (not the number 30). A quoted empty cell plus a trailing space (Ada,"" plus a space) is the same pair of Quotes errors; Papa still builds {"name":"Ada","note":"\" "} (not null). This is not the unclosed MissingQuotes-only case (Ada,"hello with no close). The page does not ignore Quotes errors and has no quote-repair mode. Nothing is uploaded.
- Why does a doubled quote inside a CSV field such as name,note / Ada,"hel""lo" become JSON hel"lo and write the JSON?
- The live converter calls Papa.parse with header: true. RFC 4180 §7 says a double-quote inside a quoted field must be escaped by another double quote (ABNF 2DQUOTE). name,note plus Ada,"hel""lo" is that pair: the "" inside the quotes is one quote, so Convert writes {"name":"Ada","note":"hel\"lo"} and there is no Quotes error. JSON.stringify writes the quote as \" (RFC 8259). The RFC example "aaa","b""bb","ccc" becomes {"a":"aaa","b":"b\"bb","c":"ccc"}. A quote at the start (Ada,"""hello") is {"name":"Ada","note":"\"hello"}. A quote at the end (Ada,"hello""") is {"name":"Ada","note":"hello\""}. A field that is only a quote (Ada,"""") is {"name":"Ada","note":"\""} (not null; a quoted empty Ada,"" is still null). A comma inside the escaped field (Ada,"hel"",lo") stays in note ({"name":"Ada","note":"hel\",lo"}). A doubled quote in a header (name,"no""te" plus Ada,hello) becomes the key no"te. A single inner quote without doubling (Ada,"hel"lo") is InvalidQuotes ("Trailing quote on quoted field is malformed"); Papa still builds {"name":"Ada","note":"hel\"lo"}, but the live page treats that Quotes error as fatal and writes no JSON. A backslash is not RFC 4180 (Ada,"hel\"lo") and is also InvalidQuotes. An unquoted inner quote (Ada,hel"lo) is accepted by Papa as {"name":"Ada","note":"hel\"lo"} and Convert writes that JSON (RFC 4180 §5 would forbid a quote in an unquoted field). An unquoted doubled pair (Ada,hel""lo) keeps both quotes ({"name":"Ada","note":"hel\"\"lo"}). An odd triple mid-field (Ada,"hel"""lo") is InvalidQuotes. A doubled quote inside a number-looking cell (Ada,"3""0") stays the string "3\"0" (not the number 30). This is not the trailing-space InvalidQuotes case (Ada,"hello" plus a space) and not the unclosed MissingQuotes-only case (Ada,"hello with no close). The page does not ignore Quotes errors and has no quote-repair mode. Nothing is uploaded.