skip to content

In Cucumber-JVM, how do you choose between a @DataTableType per domain type and a default entry transformer?

level: seniorimportance: should knowfreq 38%

answer

  1. something must produce the target type
  2. one converter per target type
  3. the parameter shape picks the kind
  4. defaults catch every unregistered type
  5. specific registration beats the default

basics

~20 s

Register a @DataTableType per type when the table's columns are not the object's fields or you want unknown columns to fail loudly. Register one default entry transformer when many types bind field-for-field. With neither, conversion fails naming the type.

solid answer

~40 s

A `@DataTableType` method registers a converter for exactly one target type, and its **parameter shape** decides what it converts: a header-keyed `Map<String, String>` makes an entry transformer for one row, a `String` makes a cell transformer, a `List<String>` a row transformer, and a `List<List<String>>` a whole-table transformer. `@DefaultDataTableEntryTransformer` and `@DefaultDataTableCellTransformer` instead register one catch-all each, consulted only for types nothing specific produces — so a specific registration always wins. Prefer the default when many types bind mechanically and explicit methods would be pure churn. Prefer an explicit `@DataTableType` where a column needs parsing or renaming, or where an unknown column should fail loudly rather than bind silently. With neither registered, the step fails at conversion time with a message naming the type it could not build.

code

java · 11 lines
java
@DataTableType
public MeterReading meterReading(Map<String, String> entry) {
    return new MeterReading(
            entry.get("meterId"),
            Integer.parseInt(entry.get("kwh")));
}

@Then("the corrected district-heating readings are:")
public void correctedReadings(List<MeterReading> expected) {
    assertEquals(expected, billing.readings());
}

go deeper

for a junior

Know that turning a table into your own objects needs something registered, and that @DataTableType is where that registration lives. Reading an existing transformer and seeing which type it produces is the bar here.

for a middle

Explain the four parameter shapes and the kind of transformer each produces, what the default entry and cell transformers do, and that a missing converter fails the step at conversion time rather than passing nothing.

for a senior

Show the judgement: when a suite-wide default earns its keep, when a column needing parsing or a table crossing team boundaries justifies an explicit transformer, and how to read the failure signatures when a table and a model drift apart.

for a principal

Own the policy. Decide whether feature-file tables are a strict contract on the domain model or a tolerant surface, who may register a transformer, and how that choice survives a contractor rotation without forty bespoke converters accumulating.

## What a `@DataTableType` method registers `@DataTableType` marks a method in your glue as a converter for **one target type**, and the *shape of its single parameter* decides which slice of the table it converts: | Parameter shape | Kind of transformer | Converts | |---|---|---| | `Map<String, String>` | entry transformer | One header-keyed row into one object | | `String` | cell transformer | One individual cell into one object | | `List<String>` | row transformer | One row of cells into one object, no header | | `List<List<String>>` | table transformer | The whole table into one object | The return type is the target. Registering an entry transformer that returns `MeterReading` is what makes a step method declaring a list of `MeterReading` work: Cucumber consumes the first row as the header, hands each remaining row to your method as a map, and collects the results. Registration is by type, not by step. One method serves every step in the suite that asks for that type, and a second method returning the same type is a configuration error that Cucumber rejects when it assembles its converter registry — the run fails up front rather than picking one at random. ## The default transformers `@DefaultDataTableEntryTransformer` and `@DefaultDataTableCellTransformer` register a **single catch-all** each. The entry default receives a row and the type that was requested and returns an object; the cell default does the same for one cell. In practice both are wired to an object mapper that binds column names to field names. The rule that matters: **specific beats default.** A `@DataTableType` for a type always wins, and the default is consulted only for types nothing else produces. That ordering is what makes the mixed strategy safe — declare one default for the suite, then override it type by type as particular tables outgrow mechanical binding. ## What happens when nothing matches The step fails at **conversion time**, before your method body runs, with a message naming the type it could not build. It is not reported as an undefined step; the step definition matched fine and the argument is what failed. It is also not silently skipped or passed as null. That loudness is the feature: the run stops on the table rather than surfacing later as a mismatched expectation in an assertion. ## Choosing per-type or default across a real suite 1. **Count the types.** If two dozen tables map field-for-field onto DTOs, writing two dozen near-identical transformers is churn a default removes in one method. 2. **Ask whether columns equal fields.** The moment a column needs parsing, defaulting, unit conversion or renaming, a default binder either cannot express it or expresses it by contorting the model. Write the explicit transformer. 3. **Decide what column drift should cost.** A hand-written transformer reads the keys it cares about and ignores the rest, which is tolerant. A field-binding default usually rejects an unknown column, which is strict. Neither is right; pick per table family and say so out loud. 4. **Weigh who edits the feature files.** When non-engineers or a rotating contractor pool write tables, strictness turns a silent misbinding into a build failure with a name attached, which is worth the friction. 5. **Keep the registry findable.** Transformers registered in one glue class are easy to reason about; scattered across ten step classes they become the least discoverable configuration in the suite. A district-heating billing team ran a default entry transformer across 148 nightly scenarios and explicit `@DataTableType` methods for exactly three types — tariff schedules, meter readings and settlement adjustments — where columns carried units the model did not. Three weeks from a contractor handover, that split meant the incoming team had one convention to learn plus three documented exceptions, instead of forty hand-written converters. ## Across the implementation family - **cucumber-js** has no equivalent registry: the table arrives as an object and you convert inside the step, typically from `hashes()`. - **Behave** likewise leaves conversion to the step function, reading the context object's table. - **SpecFlow/Reqnroll** converts through `CreateSet<T>()` and `CreateInstance<T>()` helpers, with custom value retrievers for types the built-in binding cannot handle. Only Cucumber-JVM makes the conversion declarative and suite-wide, which is exactly why the specific-versus-default question is a Cucumber-JVM question and not a general BDD one. ## Failure signatures worth recognising - Conversion failure naming a type you own — no transformer produces it; register one. - Conversion failure naming a field or property — the default binder met a column with no home. - The run failing before any scenario starts — a duplicate registration for one target type. - A table converting but every field empty — the header cells and the binder's expected names disagree.

  • How does Cucumber-JVM choose when both a @DataTableType and a default transformer could produce the same type?
    The specific registration wins. Defaults are a fallback consulted only when nothing else produces the requested type. That ordering is what makes a mixed strategy safe: declare one default for the whole suite, then override it type by type as particular tables grow columns the mapper cannot bind.
  • Two @DataTableType methods return the same type. What happens?
    Registration is keyed by target type, so a duplicate is a configuration error rather than a per-step ambiguity. Cucumber rejects it while assembling its converter registry and the run fails before any scenario executes, instead of silently picking one. Keep one transformer per type, ideally in one glue class.
  • A column is renamed and the default transformer can no longer bind it. How does that surface?
    As a conversion failure on every step using that table, naming the type or the property rather than failing an assertion. That is the useful part: the run stops on the table itself instead of reporting a mismatched expectation further down the step, so one grep on the failing name fixes the whole group at once.

saying these in an interview costs you the question

  • Thinks one @DataTableType method converts every type
  • Registers a transformer per step instead of per type
  • Expects a null or empty list when no transformer matches
  • Believes a default transformer overrides a specific one
  • Cannot say what the method's parameter shape controls