
How to Transform Rows During a CSV Import
Column matching assumes that each file column maps to one field in your schema. That works when the file and the schema split the data at the same boundaries.
A Full name column breaks that assumption. If your schema stores
firstName and lastName separately, matching alone cannot produce both
values. Your code has to transform the cell before either field receives the
right value.
For the example we will use a record label's app that stores a royalty line in six fields. The statement reference, the artist's first and last name, the track, the currency and the amount. The statement a distributor sends carries the same data in four columns.
| A | B | C | D | |
|---|---|---|---|---|
| 1 | Ref | Artist | Track | Payment |
| 2 | RS-2041 | Anna van der Berg | Slow Tide | $1,250.00 |
| 3 | RS-2042 | Ines Duarte | Harrow Lane | $980.40 |
| 4 | RS-2043 | Petr Havlik | Nightjar | $1,004.50 |
1Ref,Artist,Track,Payment2RS-2041,Anna van der Berg,Slow Tide,"$1,250.00"3RS-2042,Ines Duarte,Harrow Lane,$980.404RS-2043,Petr Havlik,Nightjar,"$1,004.50"Artist contains both name fields. Payment contains both currency and
amount. The file packs four schema fields into two columns, so a one-to-one
mapping is not enough.
Two columns hold four fields
Column matching connects Ref to the
statement reference and Track to the track automatically. Artist and
Payment have no one-to-one match, so the person importing the file maps
them to the closest fields on offer, First name for Artist and Amount for
Payment.
The import still finishes with zero validation errors. The number reader
removes the dollar sign and the grouping comma from $1,250.00, and
1250.00 is a valid amount. Anna van der Berg is a valid string for
firstName. The schema validates both values without knowing that either
cell held more than one field.
statementRef RS-2041firstName Anna van der BerglastName (empty)track Slow Tidecurrency (empty)amount 1250.00The result is structurally valid but incomplete. The currency is gone, and
the whole artist name sits in firstName.
Mapping cannot fix this, because no choice of target field splits a cell. The value itself has to be split before it reaches the fields, and the import has one place built for that code.
The callback runs after reading, before the rows land
onRowImport is a prop on the editor. The SDK calls it after parsing, column
mapping, value matching, and each column's transformer, but before the
resulting rows reach the editor.
file → parse → column mapping → value matching → transformer → onRowImport → column reader → editorFor each imported row, the callback receives two representations. row
contains the values already mapped to your schema, such as Slow Tide under
track. raw contains the original cells keyed by the file headers,
unmapped columns included.
row.track → "Slow Tide"raw["Track"] → "Slow Tide"raw["Payment"] → "$1,250.00"Both are inputs to the callback. Mutating them does not change the import.
row is a copy, and the import never reads raw again. To change the
imported row, return the fields that should be replaced.
The royalty schema has six fields. currency and amount also declare their
editor types.
import type { DataEditorColumn } from "@updog/data-editor";
export const columns: DataEditorColumn[] = [ { id: "statementRef", title: "Statement" }, { id: "firstName", title: "First name" }, { id: "lastName", title: "Last name" }, { id: "track", title: "Track" }, { id: "currency", title: "Currency", editor: { type: "currency" } }, { id: "amount", title: "Amount", editor: { type: "number" } },];Keep Payment mapped to Amount. The importer reads that column with the
number format it detected across the file, so row.amount already holds
1250.00 when the callback runs. The callback then supplies what the mapping
could not express, the two name fields and the currency.
onRowImport={(rows) => rows.map(({ raw }) => { const [firstName, ...surname] = raw["Artist"].split(" ");
return { firstName, lastName: surname.join(" "), currency: raw["Payment"].replace(/[\d\s.,'-]/g, ""), }; })}The currency line throws the number away and keeps what is left. It deletes
the digits, the spaces, the separators and the minus sign, so $1,250.00
leaves $ and EUR 1.250,00 leaves EUR. The amount survives that line
untouched, because the mapping already read it.
Each returned key replaces that field in the imported row. Fields the
callback does not return keep the values the import pipeline already
produced, so statementRef, track and the mapped amount need no
special handling.
The callback may also return a promise. The SDK waits for it before adding the rows to the editor, with a 30-second timeout per call.
After the callback, the first row carries all six fields.
statementRef RS-2041firstName AnnalastName van der Bergtrack Slow Tidecurrency USDamount 1250.00The callback split Artist across the two name fields and took the currency
symbol out of Payment, while the mapping supplied the amount. The callback
returned $, and the row stores USD. The diagram's column reader step
made that conversion, and it reads every returned value the same way.
Each returned value goes through the column's reader
Values returned from onRowImport enter the column pipeline again. The
column reader first converts them to the type the cell expects, then the
column's transformer runs on the result.
Return the value in a form the reader understands. A currency column resolves
recognised forms such as $, usd, and US Dollar to USD. A number
column accepts a JavaScript number or canonical numeric text, so both
1250.5 and "1250.00" are valid. A date column accepts an ISO date string
such as 2026-08-19. A multiselect accepts an array of values.
If the reader cannot convert a value, it leaves that value in the cell for validation to flag.
We measured that boundary on Updog Importer 0.1.90 with the royalty statement used in this example.
returned stored validation"1,250.00" 1,250.00 Invalid number"1250.00" 1250.00 —1250.5 1250.5 —"$" / "usd" / "US Dollar" USD —new Date(…) 2026-08-19T00:00:00.000Z Invalid dateThat boundary is why the callback leaves the amount to the mapping. The
file's $1,250.00 reads clean because the importer detects the column's
number format across the whole file before reading its cells. One value alone
carries no such evidence, since 1,250 is twelve hundred and fifty under one
convention and one and a quarter under another. A returned value arrives
without that verdict, so the number reader expects canonical numeric text or
a JavaScript number.
Map the column when the importer can read it, and return a value only where
mapping cannot express what the field needs. A callback that rebuilds the
amount by hand takes the file's convention on faith, and replaceAll(",", "")
turns a statement writing 1.250,00 into 1.25000, which the reader takes
as 1.25 and validation accepts. That is the opening mapping's silent loss
again, one thousandth of the payment, with nothing on screen to catch it.
An empty string clears its field. null in place of a row object removes the
row. An object past the end of the input chunk appends a new row, and its
values pass through the same reader.
The reader resolves representation. It cannot decide where
Anna van der Berg splits between firstName and lastName. That decision
belongs in onRowImport.
Splitting the name is the callback's decision
Anna van der Berg splits at the first space into Anna and van der Berg.
That rule lives in the callback. The column reader and the schema have no
opinion about where a name divides.
It is a heuristic. A Dutch particle may belong to the surname, a Spanish name may carry two family names, and a multi-word given name breaks the same rule from the other side.
Because onRowImport runs before the rows enter the editor, the result
remains reviewable. The imported row opens with Anna under First name and
van der Berg under Last name. If the heuristic gets a name wrong, the
person reviewing the statement can correct the cells before the data leaves
the editor.
The callback makes the initial split. The editor is where that decision can be checked and corrected.
One call per 5,000 rows
The SDK processes imported rows in chunks of 5,000. It calls onRowImport
once per chunk, sequentially and in file order. The final chunk contains
whatever rows remain.
The callback's second argument, meta, describes the current chunk and the
import around it. chunkIndex, chunkCount and isLastChunk locate the
chunk within the file. workbook contains the file name and its headers.
mapping contains the resolved header-to-field mapping. context is the
object passed through the editor's context prop.
meta.signal is an AbortSignal. It aborts when the import is cancelled and
when the callback exceeds its 30-second limit. A callback that starts its own
request passes the same signal to fetch, so that work stops with the
import.
await fetch(url, { signal: meta.signal,});A cancelled import then ends quietly, because the person stopped it. A callback that runs out of time fails instead.
A callback failure stops further processing. Throwing, rejecting, timing out,
or returning an invalid value produces one HOOK_ERROR through onError.
Chunks that already reached the editor remain there. The unprocessed part of
the file is restored, and the wizard stays open so the import can be retried.
Return nothing and the chunk stays unchanged. Return an array at least as
long as the input and each element answers one row, a row object to keep the
row or null to drop it. Elements past the input length append new rows.
Anything outside that contract fails the chunk. The error identifies the
invalid result where it can, element 0 is undefined included.
Nothing else runs the callback
onRowImport runs only for rows that come through the file import flow. Rows
supplied through loadData, pasted into the grid, or edited directly in a
cell bypass it.
The callback changes values alone. A returned key that matches no schema field is ignored, and no returned value attaches an error to a cell. Once the reader has taken those values, the column validators evaluate them the same way they evaluate values that came straight from the file.
Not every import needs onRowImport. If the file's columns already match the
schema boundaries, column matching is enough.
Value matching resolves option values,
and the currency reader normalizes
a currency symbol that arrives in its own column.
Use the callback when the file's row and your schema disagree about structure. One cell may hold values that belong in several fields, or several cells may need combining before the row reaches the editor.
For this royalty statement, the importer reads the file, the mapping supplies
the amount, onRowImport splits Artist and takes the currency out of
Payment, and the row opens in the grid for review before your app receives
it.
If another distributor sends First name and Last name as separate
columns, mapping covers the artist directly. The callback then keeps to the
currency.