skip to content

A Karate `Scenario Outline` is driven by a single-cell `Examples:` table holding `read('users.csv')`, and the step `match __row.age == 30` fails. Why, and what are your options?

level: seniorimportance: should knowfreq 38%

answer

  1. The reader returns text, not values
  2. There is no header left to hint
  3. The message cannot show quotation marks
  4. Fix it at the source, not per step
  5. Pin the row's shape once per outline

basics

~20 s

Karate's CSV reader hands back every field as a string, so __row.age is '30', not 30, and a strict match against a number fails. CSV carries no types, and a dynamic table has no ! header to hint one.

solid answer

~50 s

Karate reads a CSV into a list of maps in which **every field is a string** — the reader returns raw text and nothing converts it — so the row's `age` is `'30'`. `match` is strict about type, and the failure message shows `30` against `30` because it cannot draw quotation marks around a type. The `!` type hint that would fix this is a **column-header** feature of a hand-written `Examples:` table, and a dynamic single-cell table has no headers at all. Three ways out, in the order I would try them: move the fixture to **JSON**, where types are already in the file; **convert inside the feature**, either per step or by mapping the list before it becomes the table; or **assert against the string** and accept that the fixture is untyped. JSON is usually right.

code

gherkin · 12 lines
gherkin
Feature: converting an untyped fixture once

@setup
Scenario:
* def raw = [{ name: 'Alice', age: '30' }, { name: 'Bo', age: '41' }]
* def users = karate.map(raw, function(r){ r.age = parseInt(r.age); return r })

Scenario Outline: <name> arrives typed
* match __row == { name: '#string', age: '#number' }

Examples:
| karate.setup().users |

go deeper

for a junior

Recall that a CSV read gives you strings for every field, so a match against a number fails even when both sides look identical in the message.

for a middle

Explain why the ! type hint cannot help: it is a column-header feature and a dynamic table has no header, so the types are whatever the cell's expression produced.

for a senior

Fix it at the boundary. Prefer a JSON fixture; if the CSV is someone else's, convert once in an @setup scenario, and pin the row shape so the next drift fails loudly.

for a principal

Decide who owns the fixture format. A CSV a domain expert maintains buys collaboration and costs a conversion layer, and that trade should be made deliberately, not discovered.

## Why the match fails Two independent facts meet here, and each one alone is harmless. 1. **A CSV has no type information.** Karate's reader walks the records and puts each field into the row map exactly as the parser handed it over — a `String`, always. A column of `30` and a column of `Alice` are indistinguishable to it. 2. **`match` is strict about type.** `'30' == 30` is false, and the failure message renders both sides without quotation marks, so it reads as `30` against `30`. That is the single most time-wasting failure shape in a data-driven Karate suite. The instinct is to reach for the `!` type hint. It does not apply. The hint lives on a **column header** of a literal `Examples:` table, and a dynamic table is a single cell holding an expression — there is no header row to mark. Nothing in a dynamic table types anything; the types are whatever the expression produced. ## What each source actually gives you | Cell | Row values | Types | |---|---|---| | `read('users.json')` | one array element | from the file: numbers stay numbers | | `read('users.csv')` | one CSV record | **all strings**, always | | `karate.setup().data` | one element of a variable built in steps | whatever the steps built | | a literal `Examples:` table | one row | strings, unless the header carries `!` | So the *same outline body* behaves differently depending only on which file the cell reads, and nothing in the feature says so. That is the design constraint worth naming in review. ## The options, in the order to try them 1. **Move the fixture to JSON.** A JSON array already carries the types, `read('users.json')` drops straight into the same cell, and no step changes. This is right whenever the file is yours. It also makes nested data possible — a CSV cannot express an object per row at all. 2. **Convert on the way in.** When the CSV is produced by someone else — an export, a spreadsheet a domain expert maintains — map it once, in the cell or in an `@setup` scenario, rather than in every step: ```gherkin @setup Scenario: * def raw = read('users.csv') * def users = karate.map(raw, function(r){ r.age = parseInt(r.age); return r }) Scenario Outline: check <name> * match __row.age == '#number' Examples: | karate.setup().users | ``` One conversion site, expressed once, and the outline body stays clean. 3. **Convert per use.** `match parseInt(__row.age) == 30`. Cheapest to write and the worst to live with: it spreads the knowledge that this column is text across every step that touches it, and the next column someone adds will be missed. 4. **Assert against the string.** `match __row.age == '30'`. Legitimate when the value really is an opaque token — an account number with leading zeros, a postcode, a version string — and actively wrong when it is a quantity you will later compare or sum. ## How to make the class of bug loud The fix for one column is easy; the fix for the *category* is a row-shape assertion. Put one step at the top of the outline that pins the shape of every row: ```gherkin * match __row == { name: '#string', age: '#number', active: '#boolean' } ``` Now the failure lands on the row, naming the field, before any business assertion runs. That converts a puzzling `30` versus `30` several steps in to a single obvious message at the point of entry, and it catches the same problem when someone adds a column, swaps the fixture for a CSV export, or drops a `!` from a static table. ## The judgement behind the fix - **Prefer the source that carries types.** Every conversion step is a place where the fixture and the assertion can drift apart. JSON removes the class of bug rather than handling it. - **Convert once, at the boundary.** If the CSV is not yours, the boundary is where it enters — the `@setup` scenario or the cell — not every assertion downstream. - **Do not silently loosen the assertion.** Changing `== 30` to `== '30'` to make a red run green hides a real question: is this field a number to this system or not? Answering that is the point of the failure. - **Watch for the same shape elsewhere.** A hand-written table missing a `!`, a CSV export someone swapped in for a JSON fixture, and a `@setup` scenario that stringified a value all produce the identical symptom. One row-shape assertion covers all three. The broader rule: in a dynamic Karate outline, the row's types are decided entirely by the expression in the cell, and nothing downstream of the cell will type them for you.

  • Why can't you add a `!` type hint to a Karate dynamic `Examples:` table to fix this?
    The hint is part of a column header, and a dynamic table has no header row — it is one cell holding an expression. The rows come out of that expression already typed, or already not. Adding `!` anywhere in the cell just changes the expression text, and it will fail to evaluate rather than type anything.
  • When is asserting against the string form the right answer rather than a cop-out?
    When the field is genuinely an opaque token rather than a quantity: an account number with meaningful leading zeros, a postcode, a version string, an external reference id. Converting those to numbers loses information. The test is whether anything in the system ever does arithmetic or ordering on the value — if not, text is the honest type.
  • A Karate outline passed for months and starts failing after someone swapped its JSON fixture for a CSV export. What single change would have caught it at the boundary?
    A row-shape assertion as the outline's first step — `match __row == { name: '#string', age: '#number' }`. It fails on the row, naming the field whose type changed, instead of surfacing as a confusing equality failure inside a business assertion several steps later.

saying these in an interview costs you the question

  • Assumes Karate infers numbers when it reads a CSV file
  • Tries to add a ! type hint to a dynamic single-cell Examples table
  • Loosens the assertion to the string form without asking whether it is a number
  • Converts the field inside every step that touches it
  • Reads the failure message as a genuine value mismatch rather than a type one
  • Thinks the row-shape check belongs in a separate scenario rather than the outline