CSV to JSON
Convert a CSV table into JSON objects with Papa Parse. First row is headers. Not Excel or JSON-to-CSV.
Docify's CSV to JSON converter turns a pasted or locally read table into an array of objects in the browser with Papa Parse: header: true, skipEmptyLines: true, and dynamicTyping: true. The result is pretty-printed with JSON.stringify. 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); this 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 this 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 this 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 this 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 this 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 this 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; this 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 (testEmptyLine is s.length === 1 && s[0].length === 0). 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; this 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; this page writes no JSON. comments: true (or comments: '#') would skip a line that starts with #; this page does not set that option. It is not an Excel converter and not JSON-to-CSV. Nothing is uploaded.
How it works
- Paste CSV, load the sample, or choose a
.csvor.txtfile. Upload usesFileReader.readAsTextand fills the CSV box; it does not callPapa.parse(file). Nothing is sent to a server. - Convert runs
Papa.parseon that string withheader: true,skipEmptyLines: true, anddynamicTyping: true. The delimiter is left blank so Papa auto-detects from comma, tab, pipe, semicolon, or the ASCII record/unit separators (RFC 4180-style quoting). - Completely empty lines are skipped; whitespace-only rows are not (that needs
skipEmptyLines: 'greedy'). Papa'stestEmptyLinefor the boolean iss.length === 1 && s[0].length === 0— a single empty field. A blank line betweenname,age/Ada,30andBob,40is dropped, so Convert writes[{"name":"Ada","age":30},{"name":"Bob","age":40}]. A quoted empty row""is also skipped. A spaces-only row is one non-empty field; it drops the average field count below1.99(UndetectableDelimiterplusTooFewFields) and this page writes no JSON. A comma-only row,is two empty fields ({"name":null,"age":null}) and Convert succeeds. This page does not enable greedy skip. Numbers that look like decimal literals strictly inside±2^53and the stringstrue/falseare typed; other cells stay strings. A padded ID such as007matches that FLOAT test, soparseFloatturns it into7and the row becomes{"zip":7}. Quoted CSV"007"is still7.00123becomes123.00becomes0.007astays"007a". A date-only cell such as2026-09-18stays a string. This page does not turn offdynamicTyping. - That same FLOAT test accepts an optional minus, not a leading plus, so
n,+30becomes{"n":"+30"}. RFC 4180 quotes around"+30"are stripped first, so a quoted+30is still the string"+30".-30becomes the number-30.30becomes30.+30.5and+007stay strings. Plus is legal in an exponent, so1e+2becomes100;+1e2stays"+1e2". JSON has no plus-prefixed number (RFC 8259 §6). - That same FLOAT regex requires one or more digits after
eorE([eE][-+]?\d+), son,1ebecomes{"n":"1e"}. RFC 4180 quotes around"1e"are stripped first, so a quoted1eis still the string"1e".1e+stays"1e+".1e-stays"1e-".1Estays"1E".-1estays"-1e".1e+2becomes the number100.1e2and1E2are also100.1e-2becomes0.01.1e+0becomes1..5estays".5e";1.estays"1.e"(.5e2and1.e2are complete and become50and100).1e+2.5stays"1e+2.5".e2stays"e2". Surrounding spaces are kept, so1estays" 1e "— the regex never matches, soparseFloatis not called.parseFloatof"1e"or"1e+"would be1(they stop at the incomplete exponent); Papa never takes that path.JSON.parseof"1e"or"1e+"throws (RFC 8259 §6 exp needs one or moreDIGIT).JSON.parseof"1e+2"is100. Write1e+2or100for the number. In a semicolon table,n;xplus1e;foois{"n":"1e","x":"foo"}. A CRLF1eis the same . This page does not coerce an incomplete exponent. - That same FLOAT regex has no
0xor0Xhex prefix, son,0x1becomes{"n":"0x1"}. RFC 4180 quotes around"0x1"are stripped first, so a quoted0x1is still the string"0x1".0XFFstays"0XFF".0xffstays"0xff".-0x1stays"-0x1".+0x1stays"+0x1".0xstays"0x".0x10stays"0x10"(not16).0b10stays"0b10".0o10stays"0o10". Surrounding spaces are kept, so0x1stays" 0x1 "— the regex never matches, soparseFloatis not called.parseFloatof"0x1"would be0(it stops at thex); Papa never takes that path.NumberandparseIntof"0x1"are1.JSON.parseof"0x1"throws (RFC 8259 §6 has no hex prefix). YAML 1.2.2 Core0x [0-9a-fA-F]+would turn0x1into1; this page does not. Write1for the number (or16for0x10). In a semicolon table,n;xplus0x1;foois{"n":"0x1","x":"foo"}. A CRLF0x1is the same. This page does not coerce a hex prefix. - That same FLOAT regex matches a complete exponent such as
1e16(eplus one or more digits). After the match,testFloatcallsparseFloatand keeps a Number only whenfloatValue > MIN_FLOAT && floatValue < MAX_FLOAT, whereMAX_FLOATis2^53.parseFloatof"1e16"is10000000000000000, which is not< MAX_FLOAT, son,1e16becomes{"n":"1e16"}. RFC 4180 quotes around"1e16"are stripped first, so a quoted1e16is still the string"1e16".1e+16stays"1e+16".1E16stays"1E16".-1e16stays"-1e16".1.0e16stays"1.0e16".10e15stays"10e15"(parseFloatis also1e16).1e15becomes the number1000000000000000.9e15becomes9000000000000000.1e+2still becomes100. If the exclusive-range check fails, the original cell is kept, so1e16stays" 1e16 "— the regex matched andparseFloatran, but1e16is not< MAX_FLOAT.+1e16stays"+1e16"because of the leading plus, not this range check.JSON.parseof"1e16"is the number10000000000000000(RFC 8259 §6 allows exp). Write1e15for the number1000000000000000, or9007199254740991for the largest typed integer. In a semicolon table,n;xplus1e16;foois{"n":"1e16","x":"foo"}. A CRLF1e16is the same. This is not the incomplete-exponent1ecase and not the underflow1e-324case. This page does not parse integers as BigInt. - That same FLOAT regex also matches a complete tiny exponent such as
1e-324(e, a minus, and one or more digits). After the match,testFloatcallsparseFloatand keeps a Number only whenfloatValue > MIN_FLOAT && floatValue < MAX_FLOAT.parseFloatof"1e-324"is0(IEEE-754 underflow;Number.MIN_VALUEis5e-324).0is strictly inside±2^53, son,1e-324becomes{"n":0}. RFC 4180 quotes around"1e-324"are stripped first, so a quoted1e-324is still0.1e-325is also0.2e-324is0(rounds toward zero).5e-324becomes5e-324.4e-324becomes5e-324.1e-323becomes1e-323.1e-308becomes1e-308.1E-324and1.0e-324are also0.10e-325is0.-1e-324is JavaScript-0;JSON.stringifywrites0.1e-324becomes0— the regex matched andparseFloatran, and0is inside the range.+1e-324stays"+1e-324"because of the leading plus, not this underflow.JSON.parseof"1e-324"is also0(RFC 8259 §6 allows exp). Write5e-324forNumber.MIN_VALUE. In a semicolon table,n;xplus1e-324;foois{"n":0,"x":"foo"}. A CRLF1e-324is the same. This is not the complete-exponent1e16case (that original string is kept) and not the incomplete-exponent1ecase. This page does not keep underflow as a string. - That same FLOAT test also accepts a leading decimal such as
.5or a trailing decimal such as1., son,.5becomes{"n":0.5}andn,1.becomes{"n":1}. RFC 4180 quotes around".5"are stripped first, so a quoted.5is still0.5. A quoted1.is still1.-.5becomes the number-0.5.-1.becomes-1.0.5stays0.5.1.0becomes1. A lone.stays the string".".1.2.3stays"1.2.3"..5e2becomes50;1.e2becomes100.JSON.parserejects.5and1.(RFC 8259 §6 requires an integer part and at least one digit after a decimal point). - That same FLOAT regex has no underscore, so
n,1_000becomes{"n":"1_000"}. RFC 4180 quotes around"1_000"are stripped first, so a quoted1_000is still the string"1_000".1000becomes the number1000.1_000.5stays"1_000.5".-1_000stays"-1_000".+1_000stays"+1_000".1_000_000stays"1_000_000".1_000e2stays"1_000e2". Surrounding spaces are kept, so1_000stays" 1_000 "— the regex never matches, soparseFloatis not called.parseFloatof"1_000"would be1(it stops at the underscore); Papa never takes that path.JSON.parseof"1_000"throws (RFC 8259 §6 has no digit separator). Write1000for the number. In a semicolon table,n;xplus1_000;foois{"n":"1_000","x":"foo"}. Unquoted1,000in a comma table isTooManyFields. This page does not strip underscores. - After that FLOAT regex matches, Papa's
testFloatcallsparseFloatand keeps a Number only whenfloatValue > MIN_FLOAT && floatValue < MAX_FLOAT, whereMAX_FLOATis2^53(9007199254740992). Those bounds are exclusive, so9007199254740991becomes the number9007199254740991andn,9007199254740992becomes{"n":"9007199254740992"}. RFC 4180 quotes around"9007199254740992"are stripped first, so a quoted9007199254740992is still the string"9007199254740992".-9007199254740991becomes the number-9007199254740991.-9007199254740992stays"-9007199254740992".9007199254740993stays"9007199254740993"becauseparseFloatof that digit string is9007199254740992, which is not< MAX_FLOAT.JSON.parseof"9007199254740992"is the number9007199254740992(exactly representable);JSON.parseof"9007199254740993"is9007199254740992(IEEE-754). This page does not parse integers as BigInt. - That same FLOAT regex allows surrounding whitespace (
/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/), son, 30becomes{"n":30}.parseFloat(" 30 ")is 30. RFC 4180 quotes around" 30 "are stripped first, so a quoted30is still30. A leading-only space and a trailing-only space are also30.-30becomes-30..5becomes0.5.1.becomes1.1e+2becomes100. An internal space such as3 0stays"3 0". A space-only cell stays" ". Papa's boolean test is exact equality, sotruestays" true ". Papa'sISO_DATEtest is a full-string match, so2026-09-18T12:00:00Zstays a string. If the exclusive-range check fails, the original cell is kept, so9007199254740992stays" 9007199254740992 ".9007199254740991becomes9007199254740991.JSON.parseof" 30 "is also30(RFC 8259 insignificant whitespace). This page does not trim cells beforedynamicTyping. - That same FLOAT
\sincludes tab, but an unquoted both-side tab around30on a one-column header is first a delimiter. Papa leaves the delimiter blank, soguessDelimitertries comma, tab, pipe, semicolon, and the ASCII record/unit separators, and keeps a candidate only when the average field count is greater than1.99. Headernplus an unquoted tab,30, and a tab is three fields, so tab wins and the row isTooManyFields(expected 1 fields but parsed 3). This page shows that message and writes no JSON. RFC 4180 quotes keep those tabs inside one cell, so tab is not guessed.parseFloat("\\t30\\t")is 30, so a two-column comma tablen,xplus a quoted tab-padded30andfoobecomes{"n":30,"x":"foo"}. The same two-column table with an unquoted tab-padded30is also30because 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, butUndetectableDelimiterdefaults to comma and this page still writes no JSON. A quoted tab-paddedtruestays a string (boolean is exact equality). A quoted tab-padded ISO timestamp stays a string (ISO_DATEis a full-string match). If the exclusive-range check fails, the original cell is kept, so a quoted tab-padded9007199254740992stays that tab-padded string.JSON.parseof a tab-padded30is also 30 (RFC 8259 insignificant whitespace). This page does not expose a delimiter picker. - A clean one-column table is a different delimiter warning. Header
namethenAdaaverages 1 field, soguessDelimiterkeeps no candidate (threshold1.99). Papa still defaults to comma andresult.datais[{"name":"Ada"}], but Convert treats anyresult.errorsas fatal (errors.length > 0), soUndetectableDelimiter(Unable to auto-detect delimiting character; defaulted to ',') is shown and this page writes no JSON.zipthen007is the same warning; Papa would type[{"zip":7}]but the page still writes no JSON. Quoted"name"/"Ada"is the same. Two-columnn,xplus30,fooaverages 2 fields, comma wins, there is no warning, and Convert writes[{"n":30,"x":"foo"}]. Adding a dummy second column (name,xthenAda,) becomes[{"name":"Ada","x":null}]because the empty cell is null, and Convert succeeds. This is notTooManyFields. This page does not ignore delimiter warnings and has no delimiter picker. - A two-column semicolon table is the success path for that same
guessDelimiterlist.name;ageplusAda;30averages 2 fields, so semicolon wins. There is noUndetectableDelimiter, and Convert writes[{"name":"Ada","age":30}].name|ageplusAda|30is the same with pipe. Papa's FLOAT regex is/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/— a decimal point, not a comma — son;xplus3,14;foobecomes{"n":"3,14","x":"foo"}. RFC 4180 quotes around"3,14"are stripped first, so a quoted3,14is still the string"3,14". Write3.14for a JSON number:n;xplus3.14;foobecomes{"n":3.14,"x":"foo"}. A quoted"3.14"is still3.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,ageplusAda;30isUndetectableDelimiterplusTooFewFields({"name":"Ada;30"});name;ageplusAda,30isUndetectableDelimiterplusTooManyFields. Convert treats those errors as fatal and writes no JSON. Unquoted3,14in a comma table is alsoTooManyFields(3and14are two fields). A one-columnnotethenhello;worldkeeps the semicolon inside one cell and is stillUndetectableDelimiter. A space after the semicolon (Ada; 30) becomes the number30because FLOAT allows surrounding whitespace.zip;xplus007;foois still{"zip":7,"x":"foo"}. An empty cell after a semicolon (Ada;) isnull. This is not the tab-padded one-columnTooManyFieldsand not the one-columnname/AdaUndetectableDelimiter. This page does not expose a delimiter picker or a decimal-comma locale toggle. - Duplicate headers are renamed so later columns do not overwrite earlier ones. The first
namestaysname; a laternamebecomesname_1, thenname_2.name,nameplusAda,Bobbecomes{"name":"Ada","name_1":"Bob"}. RFC 4180 quotes around"name"are stripped first, so a quoted"name","name"is the same. A thirdnamebecomesname_2({"name":"Ada","name_1":"Bob","name_2":"Cam"}). Ifname_1already exists as a real header (name,name_1,name), the laternamebecomesname_2.nameandNameare different keys (case-sensitive). A leading space (name, name) is a different key. Two empty headers become""and_1.meta.renamedHeadersis{ name_1: "name" }, but this page does not expose that map ortransformHeader.JSON.parseof{"name":"Ada","name":"Bob"}keeps the last value (RFC 8259 §4); Papa avoids that overwrite. There is noFieldMismatch; Convert writes the JSON. - After each data row is built, Papa compares the parsed field count with the header count.
name,age,cityplusAda,30isTooFewFields(Too few fields: expected 3 fields but parsed 2). Papa still builds{"name":"Ada","age":30}and omitscity(the missing key is notnull). Convert treats anyresult.errorsas fatal (errors.length > 0), so thatFieldMismatchis shown and this page writes no JSON. A one-cell rowAdaisTooFewFieldsexpected 3 parsed 1 ({"name":"Ada"}). RFC 4180 quotes around"Ada"do not change the count. A trailing comma (Ada,30,) is three fields: the emptycitybecomes JSONnulland 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 asname,ageplusAda,30,extraisTooManyFields(Too many fields: expected 2 fields but parsed 3); Papa stores the extra cell on__parsed_extraas["extra"]. Two extras become["extra","more"]. A quoted extra is the same. This is not the tab-padded one-columnTooManyFields(that is delimiter detect). This page does not ignoreFieldMismatchand has no partial-output mode. - A field that starts with a quote must find a closing quote before EOF (RFC 4180).
name,noteplusAda,"helloisMissingQuotes(Quoted field unterminated). Papa still builds{"name":"Ada","note":"hello"}(the opening quote is skipped; the rest of the input stays innote). Convert treats anyresult.errorsas fatal (errors.length > 0), so thatQuoteserror is shown and this page writes no JSON. A closedAda,"hello"is the same object and Convert writes the JSON. A comma inside the unclosed quote (Ada,"hello,world) stays innote({"name":"Ada","note":"hello,world"}). A closed multiline quote (Ada,"hellothen a newline thenworld") is valid RFC 4180 and Convert writes{"name":"Ada","note":"hello\nworld"}. An unclosed quote that continues onto the next line (Ada,"hellothenBob,ok) swallows that line intonote({"name":"Ada","note":"hello\nBob,ok"}) and is stillMissingQuotes. This is notFieldMismatch(that isTooFewFields/TooManyFields). This page does not ignoreQuoteserrors and has no partial-output mode. - After a closing quote, Papa accepts a delimiter, a newline, or spaces then a delimiter or newline (
extraSpaces). A trailing space at EOF afterAda,"hello"is not those.name,noteplusAda,"hello"and a trailing space isInvalidQuotes(Trailing quote on quoted field is malformed) plusMissingQuotes(Quoted field unterminated). Papa still builds{"name":"Ada","note":"hello\" "}(the closing quote and the space stay innote). Convert treats anyresult.errorsas fatal (errors.length > 0), so thoseQuoteserrors are shown and this page writes no JSON. A closedAda,"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 isextraSpacesand Convert writes{"name":"Ada","note":"hello"}. A space after the close then a comma ("Ada" ,hello) is alsoextraSpacesand 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 alsoInvalidQuotes({"name":"Ada","note":"hello\"\ "}). A quoted30then a trailing space stays the string"30\" "(not the number30). A quoted empty cell plus a trailing space (Ada,""plus a space) is the same pair ofQuoteserrors; Papa still builds{"name":"Ada","note":"\" "}(notnull). This is not the unclosedMissingQuotes-only case (Ada,"hellowith no close). This page does not ignoreQuoteserrors and has no quote-repair mode. - RFC 4180 §7 says a double-quote inside a quoted field must be escaped by another double quote (ABNF
2DQUOTE).name,noteplusAda,"hel""lo"is that pair: the""inside the quotes is one quote, so Convert writes{"name":"Ada","note":"hel\"lo"}and there is noQuoteserror.JSON.stringifywrites 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":"\""}(notnull; a quoted emptyAda,""is stillnull). A comma inside the escaped field (Ada,"hel"",lo") stays innote({"name":"Ada","note":"hel\",lo"}). A doubled quote in a header (name,"no""te"plusAda,hello) becomes the keyno"te. A single inner quote without doubling (Ada,"hel"lo") isInvalidQuotes(Trailing quote on quoted field is malformed); Papa still builds{"name":"Ada","note":"hel\"lo"}, but this page treats thatQuoteserror as fatal and writes no JSON. A backslash is not RFC 4180 (Ada,"hel\"lo") and is alsoInvalidQuotes. 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") isInvalidQuotes. A doubled quote inside a number-looking cell (Ada,"3""0") stays the string"3\"0"(not the number30). This is not the trailing-spaceInvalidQuotescase (Ada,"hello"plus a space) and not the unclosedMissingQuotes-only case (Ada,"hellowith no close). This page does not ignoreQuoteserrors and has no quote-repair mode. - Papa's boolean test is exact equality:
value === 'true'orvalue === 'TRUE'becomes JSONtrue;value === 'false'orvalue === 'FALSE'becomes JSONfalse. Title-caseTrueandFalseare not those four strings, soflag,Truebecomes{"flag":"True"}andFalsestays"False". RFC 4180 quotes around"true"are stripped first, so a quotedtrueis still the booleantrue; a quotedTruestays"True".yesandYesstay strings. JSON has only lowercasetrueandfalse(RFC 8259 §3). - After those FLOAT and
true/falsetests, Papa'sISO_DATEtest matches a full-string ISO-8601 timestamp (YYYY-MM-DD,T, hours and minutes, optional seconds and a fraction, thenZor a±HH:MMoffset) and turns it intonew Date(value). Convert then pretty-printsresult.datawithJSON.stringify.Date.prototype.toJSON(ECMA-262) callstoISOString, which writes UTC with milliseconds, so2026-09-18T12:00:00Zbecomes{"when":"2026-09-18T12:00:00.000Z"}. Quoted CSV"2026-09-18T12:00:00Z"is still a Date. A space instead ofT, or a T-time without an offset, stays a string. An offset such as2026-09-18T12:00:00-05:00stringifies as2026-09-18T17:00:00.000Z. JSON has no Date type (RFC 8259). - After those FLOAT,
true/false, andISO_DATEtests, Papa'sparseDynamicreturnsvalue === '' ? null : value. An empty cell is that empty string, soname,noteplusAda,becomes{"name":"Ada","note":null}. RFC 4180 quotes around""are stripped first, so a quoted empty cell is stillnull. A space-only cell stays the string" ". The wordnullstays the string"null".N/Astays"N/A".skipEmptyLinesdrops a completely empty line, not an empty cell inside a kept row. A blank line betweenAda,30andBob,40is skipped; a spaces-only row is not.JSON.stringifywrites thenulltoken (RFC 8259 §3) and keeps the key. - Papa leaves
commentsat the defaultfalse. RFC 4180 has no comment syntax, so a line that starts with#is a data row.name,ageplus# note,ignoredplusAda,30has no error, and Convert writes[{"name":"# note","age":"ignored"},{"name":"Ada","age":30}]. A CRLF hash line is the same.#Ada,30plusBob,40is[{"name":"#Ada","age":30},{"name":"Bob","age":40}]. A one-field# commentrow (no comma) isUndetectableDelimiterplusTooFewFields; Papa still builds{"name":"# comment"}, but this page writes no JSON. A quoted"# comment"is a field value ({"name":"Ada","note":"# comment"}). A space before the hash is still a data row.// note,30is also kept.comments: true(orcomments: '#') would skip a line whose first characters are#; this page does not set that option. A hash in the header (#name,ageplusAda,30) becomes the key#name. This is notskipEmptyLines(a blank line is dropped; a#line is not). - If Papa reports any errors (for example TooFewFields / TooManyFields / MissingQuotes / InvalidQuotes), the page shows those messages and does not write JSON — even when some rows parsed. On success it pretty-prints
result.datawith 2-spaceJSON.stringify. Download saves that string asapplication/json(converted.json). This page does not convert JSON back to CSV or read Excel workbooks.
FAQ
- Does this converter upload my CSV?
- No. Parse, stringify, file read, copy, and download run in your browser. The page does not send the CSV or JSON to a server.
- How does a CSV row become a JSON object?
- 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 output is JSON.stringify of 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 (then name_2). 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 this 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 this 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 this 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 blank line between Ada,30 and Bob,40 is skipped (skipEmptyLines: true); Convert writes both rows. A spaces-only row is UndetectableDelimiter plus TooFewFields. 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 true/false into number/boolean; an empty cell becomes JSON null; other values 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; this 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 this 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 this 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 this 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 this 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; this page writes no JSON. European numbers that still use a comma as the decimal mark are not converted.
- Why does CSV 007 become JSON 7 instead of the string 007?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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). This 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?
- Papa.parse runs 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; this page 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. This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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. This page always enables dynamicTyping; there is no BigInt path. Nothing is uploaded.
- Why does CSV 30 (spaces around 30) become JSON 30?
- Papa.parse runs 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). This 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?
- Papa.parse runs 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). This 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 this 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). This page always enables dynamicTyping; there is no delimiter picker. Nothing is uploaded.
- Why does a duplicate CSV header name become 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 this 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?
- 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 this 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?
- 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 this 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?
- 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 this 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?
- Papa.parse runs 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 this 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.
- Why does a one-column CSV such as name / Ada error as UndetectableDelimiter and write no JSON?
- Papa.parse runs 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 this 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. This page does not ignore delimiter warnings and has no delimiter picker. Nothing is uploaded.
- Why does a semicolon CSV such as name;age / Ada;30 convert, while 3,14 stays the string 3,14?
- Papa.parse runs 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. This page does not expose a delimiter picker or a decimal-comma locale toggle. Nothing is uploaded.
- Why does CSV True stay the string True instead of JSON true?
- Papa.parse runs 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). This 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?
- Papa.parse runs 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). This 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?
- Papa.parse runs 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. This 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?
- Papa.parse runs 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; this 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?
- Papa.parse 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 this page 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. This 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.
- Can I convert Excel files or JSON back to CSV?
- No. Upload accepts .csv and .txt and reads them with FileReader.readAsText. There is no XLS/XLSX parser, and this page does not call Papa.unparse, so it does not turn JSON back into CSV.