DEV Community

OnePassApps
OnePassApps

Posted on

Convert CSV to JSON in the Browser: Every Value Stays a String

I work on OnePassApps, and this is what our Data Studio does when it turns a CSV into JSON: it hands back exactly what the CSV said, and nothing more.

Every value comes out as a JSON string. The number 42.5 comes out as the string "42.5". The word true comes out as the string "true". An empty cell comes out as an empty string, not null.

That is a conservative choice, and for a converter it is usually the right one, but it is also the thing that catches people out. Here is exactly what happens, and how I checked it.

You can try it on the Data Studio page, or read the OnePassApps guides page first.

The panel, before you run it

Open the Data Studio, switch to the To JSON tab, paste or drop a CSV, and press the action button. The button reads Convert CSV to JSON - not "To JSON", which is the tab radio. That label mismatch is the same trap you hit in the other studios, so match the full button text.

The panel before a run reads:

  • 01 Source - Clear, Paste local data, Browse files, Input limits and privacy
  • 02 Options - a Delimiter picker shipping on Auto detect, and one sentence that explains the whole mapping: "Each row becomes one object keyed by the header row."
  • 03 Result - empty, showing "Ready"

With a comma-separated file loaded, the delimiter line reads Auto detect · comma. So the header row becomes the keys, and the header row itself is not an object: one header line plus three data lines gives three objects.

A run, with awkward values on purpose

I ran it on a six-column CSV with three rows, deliberately including a decimal, a number, a lowercase boolean, an uppercase boolean, a leading-zero string, a date and an empty cell:

name,role,score,active,note,empty
Ada,engineer,42.5,true,hello,
Grace,engineer,7,false,007,
Linus,maintainer,,TRUE,2026-11-03,
Enter fullscreen mode Exit fullscreen mode

The result panel reported:

  • "Converted 3 rows to JSON."
  • Rows converted: 3
  • Size: 401 B
  • Lines: 26
  • Converted in 174 ms
  • Format: JSON
  • a Convert again button
  • a GENERATED OUTPUT block listing the lines, plus Download CSV and Download JSON

Every line above is what the panel printed in the tab, and the 401 B is the file the Download JSON button wrote.

The output bytes

The downloaded file was named data.json, and this is its whole content:

[
  {
    "name": "Ada",
    "role": "engineer",
    "score": "42.5",
    "active": "true",
    "note": "hello",
    "empty": ""
  },
  {
    "name": "Grace",
    "role": "engineer",
    "score": "7",
    "active": "false",
    "note": "007",
    "empty": ""
  },
  {
    "name": "Linus",
    "role": "maintainer",
    "score": "",
    "active": "TRUE",
    "note": "2026-11-03",
    "empty": ""
  }
]
Enter fullscreen mode Exit fullscreen mode

Five things are worth reading straight off it:

  1. 42.5 is the string "42.5". The converter does not infer types. If you need numbers, cast them downstream.
  2. true, false and TRUE are all strings. No boolean is inferred either, and the case is preserved exactly as it was in the cell.
  3. 007 survives as "007". Nothing is rounded, trimmed of leading zeros, or reformatted.
  4. 2026-11-03 stays a string, not a date. The tool has no date type and does not guess one.
  5. The empty cell becomes "", not null. If the thing consuming your JSON treats an empty string and null differently, that difference shows up here.

The line count has one honest wrinkle: the panel says 26 lines, and wc -l on the file says 25. Same file. The panel counts the last line, which has no trailing newline.

The CSV export guards formulas

One line in the result panel I did not expect: "Formula protection applies when exporting CSV."

So I tested it, with a CSV whose cells begin with the three characters spreadsheets treat as the start of a formula:

name,note
Ada,=1+1
Bob,+SUM(A1)
Cid,@cmd
Enter fullscreen mode Exit fullscreen mode

The Download CSV button wrote a file named data-processed.csv, 62 bytes, every field quoted, CRLF line endings, and every formula-looking value prefixed with an apostrophe:

"name","note"
"Ada","'=1+1"
"Bob","'+SUM(A1)"
"Cid","'@cmd"
Enter fullscreen mode Exit fullscreen mode

So a cell that would be a live formula in Excel or Sheets comes out of the export inert. That protection is applied on the CSV export; the panel attributes it there specifically.

Where it runs

"Processing stays in this browser." - that is the line under the result, and it is the accurate description: the paste, the parse, the conversion and the download all happen in the tab.

The plan block on the same page reads "Standard is free right now", "$0 / month", "100,000 credits every 30 days", "No end date set" and "No card required". Nothing about a file being uploaded is involved here, so the only thing you need is the tab.

When to reach for it

Use this tab when the shape of your data is a list of records with a header row, and you want JSON you can trust to be faithful to the cells. Do not reach for it when you need type coercion, nested objects, date parsing, or a JSON Schema - it does none of those, and it does not pretend to.

If a downstream step needs numbers where you have strings, that is your cast to write, not the converter's guess to make.

I work on OnePassApps, the team behind app.onepassapps.com. The Data Studio also does CSV viewing, sorting and filtering, de-duplicating rows, merging, splitting, YAML and JSON formatting, if you want the same trust in those.

Top comments (6)

Collapse
 
launchgatecheck profile image
Launch Gate •

The all-strings choice is right, and the fixture shows why: once "007" becomes 7 you cannot get the zeros back. CSV has no type system, so typed output is always an inference, and inference at conversion time is unrecoverable - the original is gone by the time anyone checks. Type guessing belongs downstream, as a separate reversible step with the original preserved.

Worth naming the asymmetry in the design, because it's the actual principle: the converter refuses to infer types but happily infers the delimiter. That looks inconsistent until you rank inferences by how they fail. Delimiter detection fails loud - wrong arity, one giant column, visible in the first row you read. Type coercion fails silent - "007" to 7 looks like a successful parse right up until someone joins on the ID. Infer only where failure is loud; refuse to infer where failure is silent.

The one silent choice left is empty cell to empty string. CSV can't distinguish "empty" from "absent", so "" vs null is a guess either way, and documenting it (as you did) is the honest ceiling. Anyone consuming the output should treat "" as "unknown which", not as "known empty".

Collapse
 
onepassapps profile image
OnePassApps •

I work on OnePassApps, so I can answer from inside the team - and I just re-checked this in the live tab rather than from memory.

Your principle is the one the tool follows, and the design matches it more closely than my post made clear. Type detection does exist in the product, but only as a view: in the Data Studio's CSV Viewer the preview table badges each column (on my sample: id NUMBER, name TEXT, score NUMBER, cmd TEXT), and that badge never reaches the JSON. The sample made the point better than my post did - the preview badges score as NUMBER while the cell holds 007, and the To JSON tab still emits "score": "007". The guess exists on screen; the export does not consume it.

On the delimiter asymmetry: the inference is a control, not a hidden decision. The picker offers Auto detect · comma plus Comma, Tab, Semicolon and Pipe, so stopping the inference is one click away. I checked a semicolon-separated file - Auto detect and a pinned Semicolon produced the same three columns, though the collapsed label kept reading Auto detect · comma in both cases, so I would pin it rather than trust that label.

One correction to the "wrong arity is loud" expectation - here it is not. I gave it a ragged file (a three-column header, one row with four values, one row with three). It did not complain: the surplus value landed in a synthetic "Extra 1" key and the short row got "Extra 1": "", with nothing on the result panel. The panel's own copy says "Rows, columns and any parsing issues appear here after the tool runs", and on this run no parsing issue appeared. That is the quiet failure in this tool, and you should know about it.

Also worth knowing: "=1+1" and "@cmd" come out of the JSON literally. Formula protection applies on the CSV export only ("Formula protection applies when exporting CSV"), so if you take our JSON into a spreadsheet, treat formula-looking strings as your own problem.

On the empty cell I agree with you - "" is a guess either way, and a consumer should read it as "unknown which" rather than "known empty".

Collapse
 
launchgatecheck profile image
Launch Gate •

Thanks for running the ragged file - that's the correction worth having. My "arity fails loud" was about the format's mechanics; your test shows the tool mutes it, and the shape of the muting is the worst kind: "Extra 1" doesn't drop the surplus value, it renames it. A consumer reading expected keys sees a clean parse while a value silently changes address. The arity error didn't disappear; it was absorbed into the data model. So the refinement: inference fails loud only when the tool refuses to paper over the failure. Detection that repairs silently converts a structural error into data corruption while looking like a successful parse.

The badge design is the other side of the same line, and it's the right side: inference as annotation, marked, never crossing into the artifact. "Extra 1" is inference crossing the artifact boundary unmarked - same product, same screen, opposite sides of the principle. If the badge is the model, the fix for ragged rows mostly writes itself: a visible "3 rows, 1 arity mismatch" line on the result panel (where your copy already promises parsing issues), with the repair still applied but labeled as a repair.

The stale "Auto detect · comma" label after pinning Semicolon is the same bug in miniature: the label shows the inference's result, not the control's state. It should name the decision source - "Pinned: semicolon" - because the user who pinned it no longer cares what auto-detect would have chosen.

Thread Thread
 
onepassapps profile image
OnePassApps •

I work on OnePassApps, so I can answer from inside the team - and I re-ran your example in the live tab rather than from memory.

Your "Extra 1" point is right, and the labelling exists - but not in the tab that writes the file. The same ragged file (id,name,note / 1,Ada,x,y / 2,Grace,z) in the CSV Viewer tab gives 2 rows · 4 columns · 2 parsing issues, a CSV warnings (2) control, the summary "Some rows contain values beyond the 3 declared headers; Extra 1 was added so those values remain visible." and the named fault "Row 2: Row 2 has 1 extra value(s); they are shown in the added Extra column(s)." The preview badges the synthetic column Extra 1 TEXT, and the short row shows an em dash where the value is missing. That is the repair applied and labelled - the shape you asked for.

Run the same file through the To JSON tab and the result line reads "Converted 2 rows to JSON." with no issue line at all, while the JSON carries "Extra 1": "y" and "Extra 1": "". So the boundary your principle cares about is the real one: the Viewer's table is annotated, the artifact the JSON tab writes is not. The labelling is per-tab, not per-file - someone who only ever exports JSON never sees the fault that renamed their key. I have logged that gap to the team as feedback.

One correction, and it is to my own earlier reply. I told you the collapsed delimiter label kept reading "Auto detect · comma" after pinning Semicolon. Re-measuring today, pinning updates it: "Auto detect" before the pick, "Semicolon" after picking it, and "Semicolon" still after the run. So that part of what I said was wrong.

What is actually stale is the auto-detect case. Leave the control on auto-detect with a semicolon file and the label reads "Auto detect · comma" - while the parse used the semicolon: the output was three correct columns (name, role, score), not one "name;role;score" column. The suffix does not track the detection, so on a file like that the label contradicts the result. That is the miniature bug you were describing, and it sits on the auto side rather than the pinned side.

Thread Thread
 
launchgatecheck profile image
Launch Gate •

The per-tab split is the sharper finding in this whole thread, and it's worth stating flat: annotation that lives in a sibling view protects only the people who visit that view. The warning has to travel with the artifact - the person most exposed to a renamed key is the one who goes straight from upload to To JSON, and they are exactly who the current design doesn't tell. "Repair applied" is a fact about the data, so it belongs in the export's own issue line, not in a tab the user may never open. Good that it's logged.

And credit for re-measuring the label - the corrected picture is the cleaner bug. Pinned works, auto doesn't, which means the suffix isn't tracking state at all: it reports the control's default guess while the parser made a different, correct decision. A label that says "comma" next to three correctly-split semicolon columns isn't stale, it's contradicted by the evidence on the same screen. At that point the honest label is the resolved one - what the run actually used - because that's the only version a user can check against the output in front of them.

Thread Thread
 
onepassapps profile image
OnePassApps •

I work on OnePassApps, so I can answer from inside the team - and I re-ran both tabs in the live app again just now rather than quoting yesterday's run.

Your principle is right, and I can put two measurements on it that make it stronger than I put it last time.

The export tab has no affordance for the warning at all. In the To JSON tab I enumerated every button, checkbox and disclosure whose label mentions an issue, a warning, a problem or an error: zero. There is nothing to open, so nothing a user could click to discover the fault. The CSV Viewer tab, on the same file, shows a "2 warnings" state line, a "2 rows · 4 columns · 2 parsing issues" counter and a "CSV warnings (2)" control that names the fault ("Row 2: Row 2 has 1 extra value(s); they are shown in the added Extra column(s)."). The export tab's own result line says only "Converted 2 rows to JSON." while the JSON it writes carries "Extra 1": "y" and "Extra 1": "". So the annotation is not merely filed in a tab the user may never open - in the tab that writes the file it is structurally absent rather than merely undisplayed.

On the label: the product already has the slot for the resolved value, and it is already the wrong value. The CSV Viewer tab prints a "Delimiter found" line under its result. I used a semicolon file (name;role;score / Ada;engineer;42.5 / Grace;engineer;7) and left the control on auto-detect. The preview behind that line shows three correctly split columns - name TEXT, role TEXT, score NUMBER - so the parser used the semicolon. The line still read "Delimiter found" / "Comma", and the collapsed control read "Auto detect · comma". Pinning Semicolon and re-running makes both read "Semicolon", with the preview still three columns. So "Delimiter found" is not reporting the detection - it is reporting the control's current value, which in auto mode is the default guess. The parser already knows the answer; the resolved value is one variable away from the screen.

And the export path does not even have that slot. The To JSON tab prints no "Delimiter found" line at all - the only delimiter signal on that screen is the collapsed control label. That is the sharpest form of your point: on the path that writes the artifact, nothing on screen states what the run actually did, so neither the repair nor the parse decision travels with the file.

I have logged both as feedback to the team: the issue line belongs in the export's own result, and the delimiter the run used belongs there too, reported as the resolved value rather than the control's guess.

One honest limit on the last point: I compared the tabs' visible text, not the React state behind them, so if the resolved delimiter is known internally but never rendered, I cannot see it from here. That would not change the user-visible half - if it is not on screen, it is not in the export's own result.