Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
CSV → Schema Mapper
Every customer hands you a messy export. Before it can flow into your system, each row must be coerced to a schema — strings to ints, "true" to booleans — and the rows that don’t fit quarantined, not silently dropped. Build the mapper. Solve it in Python or TypeScript, with hidden tests.
The problem
Implement map_rows(rows, schema). rows is a list of dicts (parsed CSV records) and schema maps each required field name to a type: "int", "float", "bool", or "str". For each row, coerce every schema field to its type. A row is valid only if all schema fields are present and every value coerces; otherwise it counts as an error. Return {"valid": [coerced rows], "errors": count}.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement map_rows(rows, schema): for each row try to build a coerced record over the schema fields; on any missing field or bad value, count it as an error instead.
Examples and constraints 4 examples
rows = [{"age":"30","name":"al"},{"age":"x","name":"bo"},{"name":"cy"}], schema = {"age":"int","name":"str"}{"valid": [{"age": 30, "name": "al"}], "errors": 2}rows = [{"ok":"true"},{"ok":"false"},{"ok":"maybe"}], schema = {"ok":"bool"}{"valid": [{"ok": True}, {"ok": False}], "errors": 1}rows = [{"n":"1","extra":"z"}], schema = {"n":"int"}{"valid": [{"n": 1}], "errors": 0}rows = [{"a":"1"}], schema = {"a":"int","b":"str"}{"valid": [], "errors": 1}- Coerce:
int("30")→30,float("1.5")→1.5,bool:"true"/"1"→True,"false"/"0"→False(anything else fails),str: leave as-is. - A missing schema field or a failed coercion makes the whole row an error (not partially valid).
- Preserve field order as given in the schema; return the count of error rows, not the rows themselves.
Hints 4 ideas
- Write a
coerce(value, type)helper that raises/throws on a bad value. - For each row, loop the schema fields in order; if a field is missing or coercion fails, mark the row an error.
- Booleans are the tricky type: only a small set of strings are valid; everything else fails.
- Return the valid coerced rows and just the count of errors.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious version leans on the built-in constructors: for each cell call int, float, bool or str, keep whatever converts, and put None where a conversion fails. It is O(R · F) time and space for R rows and F schema fields, the same as the right answer, and it is wrong in two ways. bool("false") is True in Python, because any non-empty string is truthy, so "false" and "0" both load as true. And a row with one bad cell survives as a half-filled record, while the spec says that row must be quarantined whole and counted once.
The approach
Treat each row as all-or-nothing. The reference map_rows builds a fresh record by walking schema.items() in order. A field that is not in the row raises at once, and every present value goes through _coerce(value, t), which is strict: int() and float() raise ValueError on text that is not a number, "bool" accepts only the six exact strings true, True, 1, false, False and 0, and "str" passes the value through.
The whole inner loop sits inside one try. If anything raises, the except discards the partial record and adds 1 to errors, so a row with two bad cells still counts once. record is appended to valid only after every field succeeds, and that is the invariant: everything in valid is complete, typed, keyed in schema order, and free of columns the schema does not name. The TypeScript coerce adds a /^-?\d+$/ check before parseInt, because parseInt("12abc", 10) would return 12.
Walk through an example
On the first example one row coerces cleanly, one fails coercion and one is missing a field, so the result is one valid record and two errors.
| step | row | field | value | outcome | state after |
|---|---|---|---|---|---|
| 1 | 0 | age | "30" | int("30") → 30 | record = {"age": 30} |
| 2 | 0 | name | "al" | kept as "al"; row complete | valid = [{"age": 30, "name": "al"}], errors = 0 |
| 3 | 1 | age | "x" | int("x") raises ValueError; name is never reached | record discarded, errors = 1 |
| 4 | 2 | age | absent | raises "missing" | errors = 2 |
| 5 | — | — | — | return | {"valid": [{"age": 30, "name": "al"}], "errors": 2} |
Complexity
O(R · F) time for R rows and F schema fields, since each cell is checked and converted once (strictly, each conversion is linear in the cell’s length). O(R · F) space for the coerced records in valid; the error count is O(1). The naive constructor-based version costs the same, so it is rejected for wrong answers, not for speed.
Edge cases to test
{"ok": "false"}with{"ok": "bool"}→ valid,False. Python’sbool("false")isTrue, because any non-empty string is truthy.{"ok": "TRUE"}or{"ok": "yes"}→ error. Only six exact spellings parse:true,True,1,false,Falseand0.[{"a": "1"}]with{"a": "int", "b": "str"}→{"valid": [], "errors": 1}. A missing field quarantines the whole row, even thoughacoerces.[{"n": "1", "extra": "z"}]with{"n": "int"}→{"valid": [{"n": 1}], "errors": 0}. Columns outside the schema are dropped, not counted as errors.{"n": "1.0"}as an int → error in both languages:int("1.0")raises, and the TypeScript regex wants digits only. A float-looking value is never silently truncated.{"p": ""}as a float and{"n": " 30 "}as an int: the two references disagree. Python’sfloat("")raises while JavaScript’sNumber("")is 0, and Python’sint(" 30 ")strips the spaces while the TypeScript regex rejects them. Decide whether blank means missing and whether to trim, then test it.
Common mistakes
- Using
bool(value)for booleans — any non-empty string is truthy, so"false"and"0"both becomeTrue. - Keeping a partially coerced row, or filling the failed field with
None, instead of counting the whole row as an error. - Counting one error per bad field instead of one per bad row, which double-counts a row with two problems.
- Leaving everything as strings instead of coercing to the schema’s types.
- In TypeScript, trusting
parseIntalone — it reads a numeric prefix, so"12abc"loads as 12.
Follow-ups an interviewer asks next
- Return why each row failed, not just a count.
- Collect an error record per bad row: its index, the field, the raw value and a reason such as missing or not an integer, raised as a specific exception from
_coerce. The cost stays O(R · F). Write those records to a quarantine file the customer can open, because a count says something is wrong while the list says what to fix in the export. - The headers do not match exactly: Age, a padded age, or customer_age.
- Resolve headers once per file, before any row is coerced: strip and lowercase each header, map it through an alias table to a schema field, and report unmapped headers and missing required fields up front. That check is O(columns), not O(rows), and a file missing a required column is rejected with one clear message instead of one error per row.
- The export is 10 GB.
- Stream it. Python’s
csv.DictReaderyields one row at a time, so coerce each row and write it to the valid or quarantine output immediately instead of building a list. Memory drops to O(F) for the current row plus counters, and checkpointing the row number lets a crashed run resume instead of restarting. - Add dates, enums and optional fields.
- Keep
_coercetable-driven and strict. Parse dates with an explicit format, such asdatetime.strptime(value, "%Y-%m-%d"), rather than guessing, since 03/04 means different days in different locales. Check enums by set membership. For optional fields, decide per field whether an empty string means missing, and turn it intoNoneonly where the schema allows it.
Where this shows up
The customer sends an export that “almost” fits your schema: numbers stored as text, booleans spelled three ways, a few rows missing a column. Coercing at the boundary and quarantining what does not fit is core forward-deployed and integrations work, and the same pattern runs inside every ETL loader and data-validation step. A clear count of rejected rows, traced to the column that caused them, beats a cryptic crash three layers downstream.
Reference solution
Reference solution — Python
def _coerce(value, t):
if t == "int":
return int(value)
if t == "float":
return float(value)
if t == "bool":
if value in ("true", "True", "1"):
return True
if value in ("false", "False", "0"):
return False
raise ValueError("bad bool")
return str(value)
def map_rows(rows, schema):
valid = []
errors = 0
for row in rows:
try:
record = {}
for field, t in schema.items():
if field not in row:
raise ValueError("missing")
record[field] = _coerce(row[field], t)
valid.append(record)
except Exception:
errors += 1
return {"valid": valid, "errors": errors}
print(map_rows([{"age":"30","name":"al"},{"age":"x","name":"bo"},{"name":"cy"}], {"age":"int","name":"str"}))
# -> {'valid': [{'age': 30, 'name': 'al'}], 'errors': 2}Reference solution — TypeScript
function coerce(value: string, t: string): any {
if (t === "int") {
if (!/^-?\d+$/.test(value)) throw new Error("bad int");
return parseInt(value, 10);
}
if (t === "float") {
const f = Number(value);
if (Number.isNaN(f)) throw new Error("bad float");
return f;
}
if (t === "bool") {
if (value === "true" || value === "True" || value === "1") return true;
if (value === "false" || value === "False" || value === "0") return false;
throw new Error("bad bool");
}
return String(value);
}
function mapRows(rows: any[], schema: Record<string, string>): { valid: any[]; errors: number } {
const valid: any[] = [];
let errors = 0;
for (const row of rows) {
try {
const record: any = {};
for (const [field, t] of Object.entries(schema)) {
if (!(field in row)) throw new Error("missing");
record[field] = coerce(row[field], t);
}
valid.push(record);
} catch {
errors++;
}
}
return { valid, errors };
}
console.log(JSON.stringify(mapRows([{age:"30",name:"al"},{age:"x",name:"bo"},{name:"cy"}], {age:"int",name:"str"})));
// -> {"valid":[{"age":30,"name":"al"}],"errors":2}Questions about CSV → Schema Mapper
Why does bool("false") return True in Python?
Because bool tests truthiness, not meaning. Any non-empty string is truthy, so bool("false"), bool("0") and bool("no") all return True, and only the empty string returns False. JavaScript behaves the same way: Boolean("false") is true. To read booleans from a CSV, compare the text against an explicit list of accepted spellings, as this solution does with true, True and 1 for True and false, False and 0 for False, and reject everything else so a typo is caught instead of quietly becoming True.
Should a row with one bad value be dropped or kept with a null?
For a loader like this one, quarantine the whole row. A record with a null where a required value belongs breaks the assumptions of whatever consumes it, from database NOT NULL constraints to averages that silently skip missing values, and the failure surfaces far from its cause. Keeping bad rows out of the valid set, counted and ideally listed with a reason, keeps the loaded data trustworthy and gives the customer something concrete to fix in the source export. Optional fields are the exception, and the schema should say so explicitly.
How do you validate CSV column types in Python?
The csv module returns every cell as a string, so types have to be applied after reading. Write one strict converter per type: int and float already raise ValueError on bad text, and booleans need an allowlist of spellings. Apply the converters per row inside a single try block, so one failure rejects the row and is counted once. For larger schemas, validation libraries such as pydantic let you declare the same rules as a model class and report every failing field, but the underlying idea is the same table of converters.
Why does the TypeScript solution check integers with a regex before parseInt?
Because parseInt reads a leading number and ignores the rest: parseInt("12abc", 10) returns 12 and parseInt("1.5", 10) returns 1, so bad cells would load as plausible numbers. The regex requires the whole string to be an optional minus sign followed by digits. Number is not a safe substitute either, since Number of an empty string is 0 and Number("0x1A") is 26. Python’s int is strict about trailing text but lenient elsewhere: it accepts surrounding spaces, a leading plus sign and underscores between digits.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Retry with Exponential BackoffTools time out and APIs rate-limit — a production agent retries without hammering. Exponential backoff waits longer after each failure (1s, 2s, 4s, 8s…) up to a cap. Compute the delay schedule: delay[i] = min(cap, base·2ⁱ). Solve it in Python or TypeScript.Read →
- Verify a Webhook SignatureEvery real integration sends signed webhooks — and every FDE has to verify them. Recompute the signature over the payload, compare in constant time, and reject stale events to stop replay attacks. Solve it in Python or TypeScript, with hidden tests.Read →
- Idempotency Key HandlerNetworks retry. A webhook fires twice; a payment is re-sent after a timeout. Without idempotency you double-charge or double-write. The fix: dedupe by a client-supplied key and always return the first result. Solve it in Python or TypeScript, with hidden tests.Read →
- Data Reconciliation DiffThe customer swears the migration worked. You reconcile: compare source records against what landed in the target, keyed by id, and report exactly what was added, removed, or changed. Solve it in Python or TypeScript, with hidden tests.Read →
- PII RedactorBefore customer data touches a log line — or a third-party LLM — the personal bits have to go. Mask emails, SSNs, credit cards, and phone numbers with pattern matching, leaving the rest readable. Solve it in Python or TypeScript, with hidden tests.Read →
- Config ValidatorHalf of "it doesn’t work at the customer" is a bad config — a missing key, a string where an int belongs, an env that isn’t allowed. Validate it up front and fail loudly with a clear list instead of crashing three layers deep. Solve it in Python or TypeScript, with hidden tests.Read →