
What an Importer Should Do With a Ragged CSV Row
A CSV names its columns once, in the header. A reader matches every row after it to that header by counting from the left, so the first value belongs to the first column and the second value to the second column. The count holds while every line carries as many values as the header.
A ragged row carries a different number of values than the header carries columns, and a row that carries fewer breaks the count. It appears wherever something writes a line without counting the columns. A person adds a note or a total under the last record, and that line arrives carrying one value. A program joins the values a record holds, so a record with an optional field missing comes out one value short and leaves no comma where that value would have been. A hand edit in a text editor takes out a value together with its comma, and the gap stays wherever that value stood.
Haverbrook Market keeps its stall register as a CSV, and the clerk edits it by hand. It holds fourteen rows under seven columns, and three of those lines carry fewer than seven values.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| 1 | Pitch | Trader | Stall | Goods | Pitch fee | First market | Contact |
| 2 | P-13 | Ivy Halloway | Halloway & Vane | Cheese | 42 | 2026-03-17 | [email protected] |
| 3 | P-14 | Otto Marsden | Roper Bakehouse | Bread | 38 | 2026-03-17 | [email protected] |
| 4 | P-15 | Nell Ferrand | Ferrand Flowers | Flowers | 30 | 2026-03-24 | [email protected] |
| 5 | P-16 | Rhys Coldharbour | Coldharbour Fish | Fish | 55 | 2026-03-24 | [email protected] |
| 6 | P-17 | Silas Ashgrove | Ashgrove Greens | Vegetables | 44 | 2026-03-31 | [email protected] |
| 7 | P-18 | Della Bramwell | Bramwell Preserves | Preserves | 26 | 2026-03-31 | [email protected] |
| 8 | P-24 | Cleo Windrush | Windrush Honey | Honey | 24 | 2026-04-14 | |
| 9 | P-25 | Auberon Petrie | Petrie Knives | Ironmongery | 48 | 2026-04-14 | [email protected] |
| 10 | P-26 | Marisol Enfield | Enfield Ceramics | Pottery | 35 | 2026-04-21 | [email protected] |
| 11 | P-27 | Tobias Renn | Coffee | 40 | 2026-04-21 | [email protected] | |
| 12 | P-28 | Xanthe Ollerton | Ollerton Wool | Wool | 28 | 2026-04-28 | [email protected] |
| 13 | P-29 | Bram Kettlewell | Kettlewell Pies | Pies | 46 | 2026-04-28 | [email protected] |
| 14 | P-30 | Junia Foxwell | Foxwell Herbs | Herbs | 22 | 2026-05-19 | [email protected] |
| 15 | Cash pitches are settled on the day |
1Pitch,Trader,Stall,Goods,Pitch fee,First market,Contact2P-13,Ivy Halloway,Halloway & Vane,Cheese,42,2026-03-17,[email protected]3P-14,Otto Marsden,Roper Bakehouse,Bread,38,2026-03-17,[email protected]4P-15,Nell Ferrand,Ferrand Flowers,Flowers,30,2026-03-24,[email protected]5P-16,Rhys Coldharbour,Coldharbour Fish,Fish,55,2026-03-24,[email protected]6P-17,Silas Ashgrove,Ashgrove Greens,Vegetables,44,2026-03-31,[email protected]7P-18,Della Bramwell,Bramwell Preserves,Preserves,26,2026-03-31,[email protected]8P-24,Cleo Windrush,Windrush Honey,Honey,24,2026-04-149P-25,Auberon Petrie,Petrie Knives,Ironmongery,48,2026-04-14,[email protected]10P-26,Marisol Enfield,Enfield Ceramics,Pottery,35,2026-04-21,[email protected]11P-27,Tobias Renn,Coffee,40,2026-04-21,[email protected]12P-28,Xanthe Ollerton,Ollerton Wool,Wool,28,2026-04-28,[email protected]13P-29,Bram Kettlewell,Kettlewell Pies,Pies,46,2026-04-28,[email protected]14P-30,Junia Foxwell,Foxwell Herbs,Herbs,22,2026-05-19,[email protected]15Cash pitches are settled on the dayLine 8 stops after six values. Line 11 carries six as well, and the value it is missing sits in the middle of the row. Line 15 is the clerk's own note, one value on a line of its own.
The format asks for equal width and never enforces it
RFC 4180 describes the file twice with the same verb. The fourth item of its section 2 says that each line "should contain the same number of fields throughout the file". The third item says the header "should contain the same number of fields as the records in the rest of the file". The word is lower case both times, and the document carries no RFC 2119 keyword anywhere, so nothing in it turns an uneven file into an invalid one.
So each reader had to pick a policy of its own, and they disagree.
| Reader | A row with fewer values than the header |
|---|---|
Python csv.DictReader |
fills the missing fields with restval, which defaults to None |
pandas read_csv |
reads it, because on_bad_lines counts a bad line as one with too many fields |
csv-parse |
raises CSV_RECORD_INCONSISTENT_FIELDS_LENGTH when a record holds a different number of fields than the records before it, until relax_column_count is set |
| DuckDB | names the case MISSING COLUMNS, and store_rejects writes the line into reject_errors |
Those four behaviours come from the documentation of Python 3.14.7, pandas
3.0.5, csv-parse and DuckDB, read on 2026-09-01. One of the four fills the
missing fields for you, and another does not count a short line as bad at all.
Once the fields are filled, a value missing from the middle of the row leaves
every value after it one column to the left.
Padding by position moves the values and reports the wrong cells
Line 11 is one value short, somewhere in the middle. Fill the missing field at the end, and the fee, the date and the address each land one column to the left of the field that fits them.
Pitch P-27Trader Tobias RennStall CoffeeGoods 40Pitch fee 2026-04-21First market [email protected]ContactTwo rules fire on that row. Pitch fee holds a date and fails the number check, and First market holds an email address and fails the date check. Neither cell is the one that went missing, and the person is asked to correct a fee that was never wrong.
A schema of plain text columns fires nothing at all. The row is complete, the count is right, and the trader's address is stored as the day they first traded.
The importer keeps the row's own length
Updog Importer parses a CSV with PapaParse under header: false. At version
5.6.0 PapaParse counts fields only while it builds each row into an object under
a header, and with header: false and no dynamicTyping or transform it
returns before that step. A three-column probe read in header mode answers with
Too few fields: expected 3 fields but parsed 2. Headerless, it raises no
field-count error at all, and every row reaches the import step carrying the
number of values the file wrote.
It matches each header to one of your fields before importing a row, and then measures every raw row against the header of its own file.
header values 7row values 6action alignA row carrying seven values is imported as it stands. Alignment never looks at it, and an even file never reaches any of what follows.
The schema removes the placements it cannot hold
Alignment keeps the order of the values and moves only the gaps. Six values in seven columns give seven placements of one gap, and each placement is a complete row that can be checked.
Every candidate goes through the validators on all seven columns, the gap included, which is how a required column rejects the placement that empties it. A candidate survives when every cell passes every rule that can be evaluated on the spot. The gap starts at the end of the row and walks left, and a placement dies on the first column whose rule it breaks.
The placements multiply as the gap grows, and a wide schema meeting a row that runs several values short reaches a count the engine will not enumerate. The ceiling on SDK 0.1.91 is a thousand candidates. Above it the importer skips alignment and writes the values by position, the first value into the first column, and the row keeps whatever shift the missing value caused.
Line 8 is the honey stall, which the clerk entered before there was an address for it.
line 8 P-24 · Cleo Windrush · Windrush Honey · Honey · 24 · 2026-04-14 6 values · 7 columns · 7 placements
placement first rule that rejects itgap at Contact every rule passesgap at First market First market is requiredgap at Pitch fee Pitch fee is requiredgap at Goods Pitch fee holds "Honey"gap at Stall Pitch fee holds "Honey"gap at Trader Trader is requiredgap at Pitch Pitch is requiredOne placement survives. The importer applies it and marks nothing, and the row lands with the honey stall in Stall, the fee in Pitch fee and an empty Contact, which is what the six values in the file say. Nothing on the screen mentions the repair, because one answer left nothing to ask.
A second survivor turns the repair into a question
Line 11 goes through the same seven placements against the same schema.
line 11 P-27 · Tobias Renn · Coffee · 40 · 2026-04-21 · [email protected] 6 values · 7 columns · 7 placements
placement first rule that rejects itgap at Contact Pitch fee holds "2026-04-21"gap at First market Pitch fee holds "2026-04-21"gap at Pitch fee Pitch fee is requiredgap at Goods every rule passesgap at Stall every rule passesgap at Trader Trader is requiredgap at Pitch Pitch is requiredTwo placements survive. They agree on five columns of the seven, and both put
40 in Pitch fee, the date in First market and the address in Contact. They
differ on Stall and Goods.
The importer keeps the first of the two, which leaves Coffee in Stall and
Goods empty, and it marks the two columns the survivors disagree about. A mark
puts a dashed outline on the cell and a line in the error panel. It counts as no
error, so a marked row reaches onComplete even when blockSubmitOnError is
set.
CoffeeThe five columns the candidates agree on carry no mark. Whether Coffee names
the stall or the goods sold at it is the only question the file leaves open on
that row, and it is the only question the person is asked. A failed rule fills
the cell, and the dashed outline keeps a placement question from reading as a
broken value. A screen reader adds "Misplaced: This might be in the wrong
column." to the cell announcement.
A line that fits no placement is a validation failure
Line 15 carries one value into seven columns, which is seven placements again. Four of those columns are required, and one value cannot fill four columns, so every placement leaves at least three of them empty and nothing survives.
line 15 Cash pitches are settled on the day 1 value · 7 columns · 7 placements · 0 survivors
Trader empty This field is requiredPitch fee empty This field is requiredFirst market empty This field is requiredThe importer writes the first candidate, which puts the note in the first column. It marks nothing as misplaced. Three required values are missing, and ordinary validation already says so. A missing required value is validation's business, and a mark saying the values sit in the wrong columns would answer a question this line never raised.
The validators decide how much of a row survives
Five of the seven columns in this import declare a rule, and those five are what alignment reads.
import type { DataEditorColumn } from "@updog/data-editor";
export const columns: DataEditorColumn[] = [ { id: "pitchRef", title: "Pitch", validators: [{ type: "required" }] }, { id: "trader", title: "Trader", validators: [{ type: "required" }] }, { id: "stall", title: "Stall" }, { id: "goods", title: "Goods" }, { id: "pitchFee", title: "Pitch fee", editor: { type: "number" }, validators: [{ type: "required" }, { type: "number", min: 0 }], }, { id: "firstMarket", title: "First market", editor: { type: "date" }, validators: [{ type: "required" }, { type: "date" }], }, { id: "contact", title: "Contact", validators: [{ type: "email" }] },];Pruning reads the text as the file wrote it, before any value is converted, so
it runs the rules you declare and skips the ones an editor adds for itself. A
number editor on its own leaves the column open during alignment. The
{ type: "number", min: 0 } rule on that same column removes every candidate
that puts a date or a stall name in the fee.
A function validator runs during alignment against an empty row, because the
row is still being assembled, so a rule that reads another field finds nothing
in it. An asyncFunction rule counts as a pass.
required is what settles line 11. Declared on Pitch fee and First market, it
takes the row down to two survivors and two marked cells. Without it on those
two columns, three placements survive, they disagree on Stall, Goods and Pitch
fee, and the person is asked about three cells. Both readings were measured in
Chrome on 2026-09-01 against SDK 0.1.91.
Stall and Goods hold free text and declare no rule, which is why the gap between them stays open. Two unconstrained columns side by side leave the engine nothing to tell them apart with.
A header the person leaves unmatched is unconstrained in the same way. Alignment scores each value against the schema column standing at that header's position, and a header matched to none of your fields carries no rules at all. Skipping a column in the matching step widens the placements alignment has to choose between.
The mark waits for a person
Editing a marked cell clears its mark. A right-click over that cell, or over a range or a row holding one, offers Mark as correct. It clears the marks in that scope and stays out of the undo history, because acknowledging a placement changes no value.
The filter panel gains a "Rows with misplaced values" entry the moment such a value exists, and it keeps that entry after the last mark is cleared. Running this file through the editor on SDK 0.1.91 ends with fourteen rows, one row carrying errors, three rows carrying empty cells and one row carrying a misplaced value.
A worksheet stores a rectangle
An .xlsx sheet declares the range it covers, and Updog Importer builds every row out to the width of that range. A row that ran short in the spreadsheet arrives with empty cells at the end, so its length matches the header and alignment leaves it alone. Only a delimited text file hands the importer a row of its own length, and the sample file behind this article is written as CSV for that reason.
Alignment moves values and never invents them
Line 8 lands with an empty Contact, and line 15 keeps its empty Trader. The file never carried those values, and no rearrangement of the values it did carry can produce them.
Alignment answers a placement question. A file whose bytes arrived misread carries damage inside the values themselves, and placement has no answer for that.
An export fixed at the source stops producing short rows, and every file after it arrives whole. Until then, an importer that reads the schema narrows a broken row to the question the file cannot answer, and it hands that question to the person who knows the market.
The importer decides where a value can go, and it says so when more than one answer survives. The person settles the cells the file left open. Your application receives seven fields per row, with the fee in the fee column.