Duplicate CSV Headers in the Browser: Three Naming Rules and Where They Disagree
I work on OnePassApps, so this is a measurement of our own product in the live app, not a review of someone else's code. A CSV with two headers that mean the same thing is an ordinary file, not an edge case. What happen
I work on OnePassApps, so this is a measurement of our own product in the live app, not a review of someone else's code.
A CSV with two headers that mean the same thing is an ordinary file, not an edge case. What happens next - which name survives, which name gets a suffix, and whether the app tells you - is where the interesting behaviour lives. I measured it in the live Data Studio on 4 November 2026.
There are three places in that studio where a column name is decided, and as of this build they do not use the same rule. You can follow along in the live tool: https://app.onepassapps.com/studios/data?utm_source=devto&utm_medium=post&utm_campaign=csv-column-naming&utm_content=studio-link
What I measured, and how
Every case below is a real file. I pasted it into the studio, pressed the tool's own button, and read the bytes of the file it handed me back. Where I quote the app, the quote is what it printed.
- The duplicate path - two headers in one file with the same name.
- The added-column path - a row with more values than there are headers, so the app has to invent a column.
- The rename path - me renaming a column by hand in the Columns tool.
The source in every rename case was the same file: headers id,Extra 1 and the row a,b,surplus. The app's own repair of that file gives the table two columns named Extra 1 and Extra 1_2 before I touch anything.
The duplicate path: first free slot, in the Extra 1 family
Headers Extra 1,Extra 1 with a third value in the row produced three keys:
[
{
"Extra 1": "a",
"Extra 1_2": "b",
"Extra 1_3": "c"
}
]
The second header - the duplicate - became Extra 1_2. The third value, which had no header at all, became Extra 1_3. The Viewer tab named both events:
Row 1: Duplicate header βExtra 1β is shown as βExtra 1_2β.
Some rows contain values beyond the 2 declared headers; Extra 1_3 was added so those values remain visible.
The suffix is the first free slot in the family, not the next one after the highest number in play. With headers Extra 1,Extra 1_2 the same surplus value produced Extra 1_3 again, so the reader's own _2 was stepped over rather than reused. And with headers id,Extra 1_2 the surplus value produced the plain Extra 1 - the tool's own column took the lower free slot even though the reader had already typed _2 by hand.
The added-column path: a fixed base, and the same ladder
The added column's base name is the literal string Extra 1, whatever the file's headers are:
| Headers in the file | Row | Keys in the JSON |
|---|---|---|
id,name |
a,b,c |
id, name, Extra 1
|
Extra 1,id |
a,b,c |
Extra 1, id, Extra 1_2
|
id,Extra 1_2 |
a,b,c |
id, Extra 1_2, Extra 1
|
Two things fall out of that table. The base is not derived from the reader's headers - it is Extra 1 in every case. And the path reserves across the whole family: when the duplicate path had already taken Extra 1_2 in the same parse, the added column took Extra 1_3 instead of colliding.
The rename path: it folds case, and it knows the tool's own names
I renamed the id column three times, each attempt on a fresh load of the studio, then renamed a free name as a control:
| Renamed to | Result | CSV that came back |
|---|---|---|
Extra 1 |
refused - "Choose a unique column name." |
"id","Extra 1","Extra 1_2" (unchanged) |
extra 1 |
refused - "Choose a unique column name." |
"id","Extra 1","Extra 1_2" (unchanged) |
Extra 1_2 |
refused - "Choose a unique column name." |
"id","Extra 1","Extra 1_2" (unchanged) |
Name |
applied - "Column name updated in the active table." | "Name","Extra 1","Extra 1_2" |
extra 1 is the lower-case spelling of a name already in the table, and the rename refused it with exactly the same message as Extra 1. So the rename path compares case-insensitively. It also refuses Extra 1_2 - the name the app itself invented for the surplus column - so it reserves against the tool's own output too.
Where the three rules disagree
| Path | Comparison | Reserves against |
|---|---|---|
| Duplicate | case-sensitive | the reader's headers, first free _N
|
| Added column | case-sensitive | the reader's headers and the duplicate path's names, first free _N
|
| Rename | case-insensitive | every name in the table, including the app's own added column |
Two of the three compare case-sensitively and one does not, over one name space. The visible consequence is a file where two keys differ only by case.
Headers extra 1 with a surplus value produced:
[
{
"extra 1": "a",
"Extra 1": "b"
}
]
Two keys - the reader's header and the app's added column - differing only in the case of the E. The app's warning for that file was the generic one:
Some rows contain values beyond the 1 declared headers; Extra 1 was added so those values remain visible.
It does not say that Extra 1 and the reader's extra 1 are the same name in two spellings.
The same gap shows up the other way round. Headers extra 1,Extra 1 produced two distinct columns, extra 1 and Extra 1, and no duplicate-header warning appeared in the Viewer's warnings for that file - the duplicate path had nothing to say. Headers ID,id behaved the same way.
And the warnings live in one tab, not the one that writes
Every warning quoted above came from the Viewer tab, which prints a CSV warnings (N) block. On the To JSON tab - the tab that produces the artifact - the only line matching warning, duplicate, added or issue was the Duplicates tool label itself. The file was still offered for download.
So if you convert a CSV to JSON and something was quietly renamed, the tab that told you is a different tab from the one that wrote the file.
What I would change
One comparison rule for one name space. The rename path already has the strict behaviour the other two lack: case-insensitive, and aware of the tool's own invented names. If the duplicate and added-column paths used the same comparison, then extra 1 would either be recognised as the same name as Extra 1 - and the duplicate path would say so - or the added column would step to a name that cannot collide with the reader's.
There is a second, smaller thing: the added column's base is a fixed literal, so a reader who already has a column called Extra 1 is inside the tool's namespace by accident.
Limits, stated honestly
- One build, one browser, one session, on 4 November 2026. Every result above is from that build.
- These are file-content comparisons only. I did not test what a consumer of the JSON does with two keys that differ only by case - that is a downstream question this post does not answer.
- The case findings rest on a small set of headers:
extra 1,Extra 1,ID,id. I did not sweep the whole family. - I measured the Viewer's warnings and the To JSON tab. I did not re-run the merge tool's naming in this round.
- The rename path's case-insensitivity rests on one test:
extra 1againstExtra 1.
If you want to see the same behaviour yourself, the Data Studio is at the link above, and the app guides are at https://app.onepassapps.com/guides?utm_source=devto&utm_medium=post&utm_campaign=csv-column-naming&utm_content=guides-link
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.