Back to all postsThree rounded felt strips on cream backings, with two sage-green strips aligned around an offset brick-red strip

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.

haverbrook-market.csv
ABCDEFG
1PitchTraderStallGoodsPitch feeFirst marketContact
2P-13Ivy HallowayHalloway & VaneCheese422026-03-17[email protected]
3P-14Otto MarsdenRoper BakehouseBread382026-03-17[email protected]
4P-15Nell FerrandFerrand FlowersFlowers302026-03-24[email protected]
5P-16Rhys ColdharbourColdharbour FishFish552026-03-24[email protected]
6P-17Silas AshgroveAshgrove GreensVegetables442026-03-31[email protected]
7P-18Della BramwellBramwell PreservesPreserves262026-03-31[email protected]
8P-24Cleo WindrushWindrush HoneyHoney242026-04-14
9P-25Auberon PetriePetrie KnivesIronmongery482026-04-14[email protected]
10P-26Marisol EnfieldEnfield CeramicsPottery352026-04-21[email protected]
11P-27Tobias RennCoffee402026-04-21[email protected]
12P-28Xanthe OllertonOllerton WoolWool282026-04-28[email protected]
13P-29Bram KettlewellKettlewell PiesPies462026-04-28[email protected]
14P-30Junia FoxwellFoxwell HerbsHerbs222026-05-19[email protected]
15Cash 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 day

Line 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-27
Trader Tobias Renn
Stall Coffee
Goods 40
Pitch fee 2026-04-21
First market [email protected]
Contact

Two 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 7
row values 6
action align

A 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 it
gap at Contact every rule passes
gap at First market First market is required
gap at Pitch fee Pitch fee is required
gap at Goods Pitch fee holds "Honey"
gap at Stall Pitch fee holds "Honey"
gap at Trader Trader is required
gap at Pitch Pitch is required

One 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 it
gap 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 required
gap at Goods every rule passes
gap at Stall every rule passes
gap at Trader Trader is required
gap at Pitch Pitch is required

Two 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.

Line 11StallCoffee

This might be in the wrong column.

Line 11Goodsempty

This might be in the wrong column.

The 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 required
Pitch fee empty This field is required
First market empty This field is required

The 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.