In Cucumber-JVM, how does a data-table step argument become the parameter type your step method declares?
answer
- the method signature is the request
- Gherkin itself has no header row
- list of lists keeps every row
- list of maps keys by row one
- a domain type needs a transformer
basics
~20 sThe parameter type your step method declares drives the conversion: a list of lists gives raw rows and no header, a list of maps keys each row by the first row, and a domain-type list needs a registered transformer.
solid answer
~50 sIn Cucumber-JVM the data table written under a step is offered to the step method, and the **declared parameter type decides how it is converted**. Declare `DataTable` to get the table itself and call `asLists()`, `asMaps()` or `asList(Type)` on your own terms. Declare `List<List<String>>` and every row, the first one included, arrives as raw cell strings — there is no header, because nothing consumed one. Declare `List<Map<String, String>>` and the first row is consumed as the header, its cells becoming the keys of each remaining row's map. Declare a domain-type list and Cucumber needs a transformer that produces that type, or the step fails at conversion time with a message naming the type it could not build. The header row is therefore not a formatting nicety: it is data or keys depending purely on what you asked for.
code
java · 6 lines@When("the monthly meter readings are recorded:")
public void meterReadingsRecorded(List<Map<String, String>> rows) {
for (Map<String, String> row : rows) {
billing.record(row.get("meterId"), Integer.parseInt(row.get("kwh")));
}
}go deeper
Recall that a data table under a step is an argument to the step method, and that a list of maps is the shape that turns the first row into keys. Being able to read a converted table in glue code is the bar here.
Explain that the declared parameter type is the request and that the header row exists only because a conversion consumes it. Contrast the list-of-lists, list-of-maps and domain-type shapes and say when you would take the raw table instead.
Show the maintenance view: which shape tolerates column drift, which fails loudly, and why a conversion failure naming the target type is a better signal than an assertion failure further down the step.
Own the convention across a suite many teams edit. Decide whether shared tables bind to domain types or to maps, and how a column change is coordinated so a widening table does not fail dozens of scenarios at once.
## The declared parameter is the request A data table in a feature file is a step **argument**: the block of pipe-delimited rows written directly under a step line. Gherkin itself gives that block no structure beyond rows and cells — it has no notion of a header. The header only comes into existence because a conversion consumes it, and which conversion runs is decided entirely by the type you declare on the step method's last parameter. That is the whole rule, and stating it plainly is what separates a confident answer from a memorised list: **Cucumber-JVM converts the table into whatever the method asked for.** Change the signature and you change the semantics of the first row. ## The shapes, and what the first row becomes | Declared parameter | First row is | You receive | |---|---|---| | `DataTable` | untouched | The table object; you convert it yourself | | `List<List<String>>` | ordinary data | Every row, including the first, as a list of cell strings | | `List<Map<String, String>>` | the header | One map per remaining row, keyed by the header cells | | `List<MeterReading>` | the header | One domain object per remaining row, built by a transformer | Two traps live in that table. The first is declaring `List<List<String>>` and then being surprised that element zero is the column names — nothing was stripped, because nothing asked for it to be. The second is the reverse: declaring `List<Map<String, String>>` for a table that has no header row, which silently eats a line of real data and keys everything by it. ## Taking the DataTable yourself Declaring `DataTable` opts out of automatic conversion and hands you the table so you can convert it on your own terms: - `asLists()` — the raw grid, header included, as lists of strings. - `asMaps()` — the first row as keys, one map per remaining row. - `asList(Type)` and `asLists(Type)` — the same shapes converted to a target type. - `cells()` — the underlying cell grid. - `transpose()` — a view with rows and columns swapped, for tables written as one property per row. This is the right choice when the table is not a uniform record set: a two-column key/value block, a table whose shape you want to assert on before reading it, or a step that must report *which* cell was wrong. It also lets you convert twice — raw strings for a readable diff message, typed objects for the assertion. ## Cell content rules worth knowing - Alignment padding is trimmed, so `| 1843 |` and `|1843|` are the same cell. - A literal pipe inside a cell is escaped as `\|`, a newline as `\n`, a backslash as `\\`. - Cells are text at the point of conversion; any numeric or date meaning comes from the conversion you asked for, not from the table. ## The same problem in the other implementations | Implementation | How the table arrives | |---|---| | Cucumber-JVM | Converted to the declared parameter type, or as `DataTable` | | cucumber-js | Always a `DataTable` object; you call `raw()`, `rows()`, `hashes()` or `rowsHash()` yourself | | Behave | On the context object's table attribute; iterate rows and index cells by column heading | | SpecFlow/Reqnroll | As the table parameter, usually converted with `CreateSet<T>()` or `CreateInstance<T>()` | The point of the contrast in an interview is that only Cucumber-JVM does the conversion *from the signature*; cucumber-js hands you the object and expects you to choose. Candidates who have worked in both often describe the JVM behaviour as magic until they can name the rule. ## Where teams get burned A district-heating billing team drove monthly meter readings through a shared table used by 31 of the 148 scenarios in its nightly pack. The step took `List<Map<String, String>>`, so adding a `substation` column was free — existing steps ignored the extra key. Then a second step on the same table was rewritten to take a domain list, and the new column had no field to bind to; every scenario using that step failed at conversion rather than at an assertion. Three weeks from a contractor handover, the useful part was that the failure named the target type and stopped on the table itself, instead of surfacing twenty lines later as a mismatched expectation. The lesson generalises: `List<Map<String, String>>` is permissive and tolerant of column drift, a domain-type list is strict and tells you the moment the table and the model disagree, and `DataTable` is the escape hatch for tables that are not records at all. Choosing between them is a real design decision on a suite that more than one team edits, not a stylistic preference.
- A step declares List<List<String>> and the first element turns out to be the column names. Is that a Cucumber bug?No. `List<List<String>>` asks for the raw grid, and Gherkin has no header concept of its own — a header exists only because a conversion consumes one. If you want the first row treated as keys, declare `List<Map<String, String>>` or a domain-type list, or take a `DataTable` and call `asMaps()`.
- When would you still declare a DataTable parameter instead of a converted type?When the table is not a uniform record set: a two-column key/value block, a table you want to assert the shape of before reading, a transposed layout, or a step that must report which cell was wrong. Taking `DataTable` also lets you convert twice — raw strings for a readable diff and typed objects for the assertion.
- A column is added to a shared table but not to the domain type a step converts it into. What breaks?With `List<Map<String, String>>` nothing does; the extra key is simply present and ignored. With a domain-type list it depends on the transformer: a hand-written one ignores keys it does not read, while a default transformer binding field-for-field will usually reject the unknown column. Know which of the two a shared table sits behind before widening it.
saying these in an interview costs you the question
- Thinks Cucumber always strips the first row as a header
- Confuses the step's data table with the outline's Examples table
- Expects domain objects without registering any transformer
- Cannot say which declared shape keeps the header row
- Parses the table by splitting raw feature-file text