Data Tables to List and Map in Cucumber JVM
1. Introduction to DataTable Conversion
When Cucumber passes a Data Table from a Gherkin feature file to a Step Definition, it provides the data as a DataTable object. This object belongs to Cucumber, but most Java automation code is easier to write when the data is converted into standard Java collections. That is why DataTable conversion is an important skill in Cucumber JVM. It allows you to transform tabular Gherkin data into List<String>, List<List<String>>, Map<String, String>, or List<Map<String, String>>.
These conversions make table data easier to process. A single-column table can become a simple list. A vertical key-value table can become a map. A horizontal table with headers can become a list of maps. A generic matrix can become a list of lists. Choosing the correct conversion method makes Step Definitions cleaner, safer, and easier to maintain.
Many Cucumber beginners receive a DataTable object and then wonder how to read values from it. The answer depends on the shape of the table. The structure of the feature file should guide the Java collection type. If the table has one column, use asList(). If it is a vertical table, use asMap(). If it is a horizontal table with headers, use asMaps(). If it is a generic table without meaningful headers, use asLists().
In larger frameworks, these collection conversions are often an intermediate step. After converting to lists or maps, the framework may map values into POJOs or domain objects such as User, Employee, Product, Address, or Order. This improves type safety and keeps business logic away from raw string lookups.
2. Why Convert a DataTable?
Use Familiar Java Collections
A DataTable is a Cucumber-specific representation of table data. It is useful at the framework boundary, but most Java code naturally works with lists, maps, and objects. Converting a DataTable allows Step Definitions and helper classes to use familiar Java APIs.
For example, if a feature file lists available roles, a Java List<String> is easier to use than a raw DataTable. If a feature file provides user details as key-value pairs, a Map<String, String> is natural. If a feature file provides several employee rows with headers, a List<Map<String, String>> is readable and practical.
Conversion also improves maintainability. Index-based access such as row.get(0) is fragile when table columns change. Header-based map access such as row.get("Username") is easier to read and review. In most business-readable tables, map-based access is preferred because it preserves the meaning of the columns.
3. DataTable Conversion Flow
The conversion process starts in the feature file. Cucumber reads the table and creates a DataTable object. The Step Definition receives that DataTable and converts it into the Java structure that best matches the table shape.
Feature File
|
v
DataTable
|
v
Convert
|
+-- List<String>
+-- List<List<String>>
+-- Map<String, String>
+-- List<Map<String, String>>
|
v
Business Logic or Domain Objects
The Step Definition should not choose a conversion randomly. The structure of the Gherkin table should decide the Java collection. This prevents confusion and keeps the code aligned with the feature file.
4. Available Conversion Methods
Cucumber DataTable provides several common conversion methods. Each method fits a different table structure. Understanding these methods is essential for writing clean Step Definitions.
| Method | Returns | Best For |
|---|---|---|
| asList() | List<String> | Single-column tables |
| asLists() | List<List<String>> | Generic tables or matrix-like data |
| asMap() | Map<String, String> | Vertical key-value tables |
| asMaps() | List<Map<String, String>> | Horizontal tables with headers |
The most common conversions in enterprise Cucumber frameworks are asMap() for vertical tables and asMaps() for horizontal tables. These preserve field names and improve readability.
5. Converting to List<String>
Single-Column Tables
Use asList() when the table contains a single column of values. This is useful for simple lists such as roles, permissions, product names, statuses, or menu options.
Given the following roles
| Admin |
| Manager |
| Customer |
The Step Definition can convert the table to a list:
@Given("the following roles")
public void roles(DataTable table) {
List<String> roles = table.asList();
}
The result is a list containing Admin, Manager, and Customer. This structure is simple and appropriate because the table has only one column.
6. Accessing List Values
After converting to List<String>, values can be accessed by index or looped through. Index access is useful when the position has meaning, but loops are usually clearer when processing every value.
List<String> roles = table.asList();
System.out.println(roles.get(0)); // Admin
A loop can process all roles:
for (String role : roles) {
System.out.println(role);
}
The output is Admin, Manager, and Customer. This is useful when the scenario needs to verify that a set of roles exists or create test data for each role.
7. Converting to List<List<String>>
Use asLists() when the table is a generic grid or when headers are not meaningful. This conversion turns every row into a list of strings, and the full table becomes a list of rows.
Given the following users
| admin | admin123 |
| john | pass123 |
| peter | demo123 |
@Given("the following users")
public void users(DataTable table) {
List<List<String>> data = table.asLists();
}
The result is a list containing rows such as [admin, admin123], [john, pass123], and [peter, demo123]. This approach works, but it relies on column positions. If the table gains a new column or changes order, index-based code may break or become misleading.
8. Accessing List<List<String>> Values
With List<List<String>>, the first index selects the row and the second index selects the column.
List<List<String>> data = table.asLists();
String username = data.get(0).get(0);
String password = data.get(0).get(1);
The output for the first row is admin and admin123. This is technically correct, but the code does not reveal the meaning of column 0 or column 1. For business tables with column names, asMaps() is usually better.
Use asLists() when the table is truly generic or matrix-like. If the table has business headers, prefer map-based conversion.
9. Looping Through List<List<String>>
To process every row in a list of lists, loop through each row and access columns by index.
for (List<String> row : data) {
String username = row.get(0);
String password = row.get(1);
userService.createUser(username, password);
}
This style is acceptable for simple, headerless tables. However, it should be used carefully because the code depends on column order. If another engineer reverses the columns in the feature file, the code may still compile but behave incorrectly.
10. Converting to Map<String, String>
Vertical Tables
Use asMap(String.class, String.class) for vertical key-value tables. A vertical table represents one object where each row contains a field name and a value.
Given the following user
| Username | admin |
| Password | admin123 |
| Role | Admin |
@Given("the following user")
public void user(DataTable table) {
Map<String, String> user =
table.asMap(String.class, String.class);
}
The result is a map containing Username = admin, Password = admin123, and Role = Admin. This conversion is ideal because the table shape is key-value.
11. Accessing Map Values
After converting to a map, values are accessed by key. The key must match the first column value exactly.
Map<String, String> user =
table.asMap(String.class, String.class);
String username = user.get("Username");
String password = user.get("Password");
String role = user.get("Role");
This code is readable because the keys describe the business meaning. It is much clearer than reading values by position. However, key names are string-based, so spelling and capitalization must be consistent.
12. Looping Through a Map
A map can be looped through using entrySet. This is useful for logging, validation, or generic processing.
for (Map.Entry<String, String> entry : user.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
The output shows each field and value, such as Username and admin, Password and admin123, Role and Admin. In most business logic, you will access specific keys rather than loop through all entries, but iteration is useful in utilities and validators.
13. Converting to List<Map<String, String>>
Horizontal Tables
The most commonly used conversion for Horizontal Data Tables is asMaps(String.class, String.class). A horizontal table has headers in the first row and one or more data rows below.
Given the following employees
| Name | Department | Salary |
| John | IT | 80000 |
| Mary | HR | 70000 |
| David | Sales | 90000 |
@Given("the following employees")
public void employees(DataTable table) {
List<Map<String, String>> employees =
table.asMaps(String.class, String.class);
}
Each row becomes one map, and the headers become keys. This is readable, scalable for multiple records, and widely used in Cucumber JVM frameworks.
14. Accessing List<Map<String, String>> Values
To access a value from a list of maps, first select the row and then select the column key.
String name = employees.get(0).get("Name");
String department = employees.get(0).get("Department");
String salary = employees.get(0).get("Salary");
For the first row, the values are John, IT, and 80000. This style is much clearer than data.get(0).get(0) because the key explains what the value means.
15. Looping Through List<Map>
Most horizontal tables contain multiple rows, so looping is common. Each map represents one record.
for (Map<String, String> employee : employees) {
System.out.println(employee.get("Name"));
System.out.println(employee.get("Department"));
}
The output is John and IT, then Mary and HR, then David and Sales. In a real framework, the loop may create employees, add products to a cart, validate records, or build domain objects.
16. Which Conversion Should You Use?
The correct conversion depends on the table structure. Do not choose the method based on habit. Choose it based on the shape and purpose of the data.
| Table Shape | Example | Best Conversion |
|---|---|---|
| Single column | Admin, Customer, Manager | List<String> |
| Vertical key-value | Username - admin | Map<String, String> |
| Horizontal records | Name, Age rows | List<Map<String, String>> |
| Generic matrix | A, B, C, D grid | List<List<String>> |
17. DataTable Conversion Summary
A single-column table becomes a list. A vertical table becomes a map. A horizontal table becomes a list of maps. A generic matrix becomes a list of lists. This mental model is simple and useful.
Single Column
-> List<String>
Vertical Table
-> Map<String, String>
Horizontal Table
-> List<Map<String, String>>
Generic Table
-> List<List<String>>
If you remember this mapping, most DataTable conversion decisions become straightforward.
18. Common Mistake: Using the Wrong Conversion
A common mistake is using asMaps() for a vertical table or asMap() for a horizontal table. The conversion method should match the table shape.
// Vertical table
| Username | admin |
| Password | admin123 |
// Correct
Map<String, String> user = table.asMap(String.class, String.class);
If the table has headers and data rows, use asMaps(). If it has key-value rows, use asMap().
19. Common Mistake: Ignoring Headers
If a table has headers, using asLists() ignores their meaning and forces index-based access. This makes the code harder to read and more fragile.
| Username | Password |
| admin | admin123 |
// Weak
List<List<String>> data = table.asLists();
// Better
List<Map<String, String>> users =
table.asMaps(String.class, String.class);
Headers are there to communicate meaning. Use map-based conversion when headers exist.
20. Common Mistake: Accessing Incorrect Keys
Map keys must match table headers or field names exactly. If the table says Username and the Java code asks for User, the lookup will not return the expected value.
| Username |
// Wrong
row.get("User");
// Correct
row.get("Username");
Consistent names prevent null values and confusing failures. Teams should standardize column names across feature files.
21. Common Mistake: Storing Everything as Lists
Some teams use asLists() for every Data Table because it works for many shapes. This is technically possible, but it often creates less readable code. When table headers exist, map-based access is better.
Index-based access such as row.get(0) forces the reader to know what column 0 means. Header-based access such as row.get("Username") explains itself. In business-readable automation, clarity matters.
22. Best Practices for DataTable Conversion
Use asList() for single-column tables. Use asMap() for vertical key-value tables. Use asMaps() for horizontal tables with headers. Use asLists() only when headers are unavailable or when generic matrix processing is required.
Prefer map-based access over index-based access when the table has business field names. Convert maps into POJOs in larger frameworks. Keep Step Definitions thin by moving repeated conversion logic into mappers, factories, or helper classes.
- Match conversion method to table shape.
- Use asList() for single-column tables.
- Use asMap() for vertical tables.
- Use asMaps() for horizontal tables.
- Use asLists() for generic matrix data.
- Prefer readable keys over numeric indexes.
- Map repeated structures into POJOs.
23. Real-Time Example: Employee Registration
Employee registration is a common example of a horizontal table converted with asMaps().
Feature: Employee Registration
Scenario: Add Employees
Given the following employees
| Name | Department | Salary |
| John | IT | 80000 |
| Mary | HR | 70000 |
@Given("the following employees")
public void employees(DataTable table) {
List<Map<String, String>> employees =
table.asMaps(String.class, String.class);
for (Map<String, String> employee : employees) {
System.out.println(employee.get("Name"));
System.out.println(employee.get("Department"));
System.out.println(employee.get("Salary"));
}
}
The output is John, IT, 80000, then Mary, HR, 70000. In a real framework, each row would likely become an Employee object and be passed to an employee service.
24. Mapping Collections to POJOs
Better Type Safety
Lists and maps are useful, but they are still string-based. In larger frameworks, converting maps into POJOs makes the code cleaner and safer. A row map can become an Employee object, Product object, Address object, or Customer object.
for (Map<String, String> row : employees) {
Employee employee = new Employee();
employee.setName(row.get("Name"));
employee.setDepartment(row.get("Department"));
employee.setSalary(Integer.parseInt(row.get("Salary")));
employeeService.create(employee);
}
This is better than passing maps throughout the framework. The service layer can work with meaningful domain objects instead of raw table data.
25. Keeping Step Definitions Thin
DataTable conversion can make Step Definitions fat if too much parsing and mapping logic is placed directly in the step method. A better approach is to delegate conversion to reusable mappers.
@Given("the following employees")
public void employees(DataTable table) {
List<Employee> employees =
employeeTableMapper.toEmployees(table);
employeeService.createEmployees(employees);
}
This keeps the Step Definition short and readable. The mapper owns conversion. The service owns business execution. The Cucumber layer remains clean.
26. Designing Reusable Table Mappers
Move Repeated Conversion Out of Step Definitions
As a framework grows, the same table shapes often appear in many feature files. User tables, employee tables, address tables, product tables, and order tables may be repeated across modules. If every Step Definition converts those tables manually, the framework develops duplicated parsing logic. One method may parse Salary as int, another as BigDecimal, and another may leave it as String. This inconsistency creates maintenance risk.
A reusable table mapper solves this problem. A mapper is a small class responsible for converting a DataTable into a specific Java structure or domain object. For example, EmployeeTableMapper can convert a horizontal employees table into a List<Employee>. AddressTableMapper can convert a vertical address table into an Address object. The Step Definition only calls the mapper.
@Given("the following employees")
public void employees(DataTable table) {
List<Employee> employees = employeeTableMapper.toEmployees(table);
employeeService.createEmployees(employees);
}
This pattern improves consistency because the conversion rule lives in one place. If the table gains a DepartmentCode column or if Salary conversion changes, the mapper can be updated once. It also makes the Step Definition easier to read because the method body describes intent instead of parsing details.
Reusable mappers are especially useful when table data needs validation, default values, type conversion, enum conversion, trimming, or normalization. They give the framework a single boundary where raw Gherkin text becomes structured Java data.
27. Choosing Between Map and POJO
Maps are convenient near the Cucumber boundary, but they are not always the best structure for deeper framework logic. A map is flexible, but it is string-based. It does not tell the compiler which fields are required, which values are numeric, or which fields belong together. A POJO communicates meaning more clearly.
Use maps when the table is small, the step is simple, and the values are used immediately. For example, reading Username and Password from one vertical table may be fine as a map in a small project. Use POJOs when the data represents a real business object, when the object is passed through multiple layers, or when type conversion matters.
A service method such as employeeService.create(employee) is clearer than employeeService.create(row). The first method receives a domain object. The second receives a generic map that may or may not contain the required keys. POJOs make contracts clearer and reduce runtime surprises.
The practical rule is to accept DataTable at the Step Definition boundary, convert it quickly, and pass meaningful objects into the rest of the framework. Avoid letting raw maps spread everywhere.
28. Type Conversion After DataTable Mapping
All Table Values Start as Text
Most DataTable conversions shown in Cucumber examples use String.class. That means every value initially becomes a string, even when the business value is a number, decimal, date, boolean, enum, or amount. This is normal because feature files are text. The important part is converting the value into the correct Java type before using it in business logic.
For example, Salary may need to become int or BigDecimal. Quantity should usually become int. Price should often become BigDecimal. Role may become a Role enum. Active may become boolean or AccountStatus. If these conversions are repeated in many Step Definitions, the framework becomes noisy and inconsistent.
Employee employee = new Employee();
employee.setName(row.get("Name"));
employee.setDepartment(row.get("Department"));
employee.setSalary(Integer.parseInt(row.get("Salary")));
This conversion is acceptable in a small example, but a mapper is better for repeated use. The mapper can also produce clearer error messages when values cannot be converted. Instead of a generic NumberFormatException, it can say that Salary must be a valid number in the employee DataTable.
Type conversion should match the business meaning. Do not convert account numbers to numeric types if leading zeros matter. Do not use double for money if precision matters. Let the domain guide the Java type.
29. Handling Missing Columns and Blank Values
DataTable conversion should not assume that every feature file is perfect. A column may be missing. A key may be misspelled. A required value may be blank. A row may contain extra spaces. If the framework does not validate table content, these problems can create confusing failures later.
For map-based access, missing keys often return null. If null is passed into a Page Object or service, the failure may happen far away from the table. A mapper can fail earlier and explain the actual issue. For example, it can check that Name, Department, and Salary are present before creating an Employee object.
private String required(Map<String, String> row, String key) {
String value = row.get(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(key + " is required in the DataTable");
}
return value.trim();
}
This small validation style makes Cucumber failures easier to understand. It also prevents bad test data from silently moving through the automation flow.
30. DataTable Conversion and Parallel Execution
DataTable conversion itself is usually straightforward in parallel execution because each scenario receives its own table object. The risk appears when converted data is stored in shared mutable state. If a mapper stores rows in a static list or a shared context object without isolation, parallel scenarios can interfere with each other.
Keep converted table data scoped to the scenario. Pass it into services, scenario context, or page objects in a way that is isolated per execution. Avoid global mutable collections for table data unless the framework has a safe concurrency design.
This is especially important when DataTables create users, products, orders, or accounts. Each scenario should create or reference its own data. Do not let one scenario's converted table rows affect another scenario running at the same time.
31. Troubleshooting DataTable Conversion Issues
Common Failure Patterns
If asMap() fails or returns unexpected values, check whether the table is truly vertical with two columns. If asMaps() produces missing values, check whether the first row contains headers and whether the Java keys match those headers exactly. If asList() behaves unexpectedly, check whether the table has more than one column.
If values are null, inspect spelling, spaces, and capitalization in headers or keys. Username, UserName, and User Name are different strings. If numeric conversion fails, inspect the feature value for commas, currency symbols, spaces, or invalid characters.
If the Step Definition becomes too long while handling table conversion, that is a design issue rather than a Cucumber issue. Move parsing and validation into a reusable mapper. Keep the step focused on orchestration.
32. Validating DataTable Content
DataTable conversion should include validation when the data is important. Required columns should be present. Required values should not be blank. Numeric fields should be parseable. Enum values should be supported. Invalid table data should fail early with a clear message.
Validation should not be scattered across many Step Definitions. A mapper or helper can centralize validation rules. This improves error messages and prevents repeated parsing code.
33. DataTable Conversion and Business Readability
The choice of conversion method should support feature-file readability. If a table has meaningful headers, use a conversion that preserves those headers. If a table represents key-value fields, use a map. If a table is a list, use a list. The Java structure should reflect the business shape of the data.
Readable Gherkin and readable Java should reinforce each other. A clear table should lead to clear collection access.
34. DataTable Conversion in Large Frameworks
Large Cucumber JVM frameworks should standardize DataTable conversion patterns. Teams should know when to use asMap, asMaps, asList, and asLists. They should also know where conversion logic belongs. Without standards, different step classes may parse similar tables in different ways.
Reusable mappers are valuable in large projects. They prevent duplication, improve validation, and support POJO mapping. They also make future changes easier. If a table gains a new field, one mapper can be updated instead of many Step Definitions.
35. When Not to Use DataTable Conversion Directly
Do not keep raw lists and maps throughout the entire framework when the data represents a domain object. Raw collections are acceptable at the Cucumber boundary, but deeper layers should usually work with typed objects. This improves readability and reduces key-name errors.
Also avoid DataTables for repeated scenario execution. If each row should be a separate test case, use Scenario Outline. DataTables pass structured data into one execution.
36. Code Review Checklist
Questions Before Committing
Before committing DataTable conversion code, check whether the conversion matches the table structure. Confirm that header-based tables use map-based access. Verify that keys match exactly and that repeated conversion logic is not duplicated across many steps.
- Does the conversion method match the table shape?
- Is asMap used for vertical tables?
- Is asMaps used for horizontal tables?
- Is asList used for single-column tables?
- Is asLists used only when index-based access is appropriate?
- Are keys meaningful and consistent?
- Should the data be mapped into POJOs?
- Is parsing kept out of fat Step Definitions?
37. Interview-Ready Summary
Short Explanation for Interviews
Cucumber provides the DataTable class to represent tabular data passed from Gherkin to Step Definitions. Common conversion methods include asList() for single-column tables, asLists() for generic tables, asMap() for vertical key-value tables, and asMaps() for horizontal tables with headers.
The correct conversion depends on the structure of the table. Map-based conversions are preferred when headers or field names exist because they improve readability and reduce index-based errors. In enterprise frameworks, maps are often converted into POJOs for stronger typing and maintainability.
- DataTable is Cucumber's table object.
- asList() is for single-column data.
- asMap() is for vertical tables.
- asMaps() is for horizontal tables.
- asLists() is for generic matrix-style data.
- POJO mapping improves larger frameworks.
38. Golden Rule
The golden rule is simple: choose the collection type that matches the structure of your Data Table. Use List<String> for single columns, Map<String, String> for vertical tables, and List<Map<String, String>> for horizontal tables. Use List<List<String>> only when generic index-based matrix processing is truly appropriate.
When DataTable conversion is done correctly, Cucumber JVM Step Definitions become cleaner, table data becomes easier to process, and the framework remains readable and maintainable as scenarios grow.