CSV and Excel are flat — rows and columns. But what if your test data has structure? A bank transfer needs a source account (with type and number), a destination account, an amount, a description, and an expected result. JSON handles this nested structure naturally. It is also the format used in API testing, so you will use it constantly.
<!-- Add to pom.xml inside <dependencies> -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
<scope>test</scope>
</dependency>[
{
"testCase": "Valid transfer between own accounts",
"fromAccount": {
"type": "Savings",
"number": "1001234567"
},
"toAccount": {
"type": "Checking",
"number": "1009876543"
},
"amount": "500.00",
"description": "Monthly rent",
"expectedResult": "success",
"expectedMessage": "Transfer completed successfully"
},
{
"testCase": "Transfer to external account",
"fromAccount": {
"type": "Savings",
"number": "1001234567"
},
"toAccount": {
"type": "External",
"number": "9876543210"
},
"amount": "1000.00",
"description": "Payment to vendor",
"expectedResult": "success",
"expectedMessage": "Transfer completed successfully"
},
{
"testCase": "Insufficient balance",
"fromAccount": {
"type": "Savings",
"number": "1001234567"
},
"toAccount": {
"type": "Checking",
"number": "1009876543"
},
"amount": "9999999.00",
"description": "Large transfer",
"expectedResult": "failure",
"expectedMessage": "Insufficient balance"
},
{
"testCase": "Negative amount",
"fromAccount": {
"type": "Savings",
"number": "1001234567"
},
"toAccount": {
"type": "Checking",
"number": "1009876543"
},
"amount": "-500.00",
"description": "Invalid negative",
"expectedResult": "failure",
"expectedMessage": "Amount must be positive"
},
{
"testCase": "Zero amount",
"fromAccount": {
"type": "Savings",
"number": "1001234567"
},
"toAccount": {
"type": "Checking",
"number": "1009876543"
},
"amount": "0",
"description": "Zero transfer",
"expectedResult": "failure",
"expectedMessage": "Amount must be greater than zero"
}
]Notice how fromAccount and toAccount are nested objects. Try doing that in a CSV. You would need columns like fromAccount_type, fromAccount_number — ugly and hard to read. JSON makes hierarchical data clean.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.testng.Assert;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class BankingTransferJsonTest extends BaseTest {
@DataProvider(name = "transferData")
public Object[][] readTransferData() throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(
new File("src/test/resources/test-data/"
+ "transfer-data.json"));
List<Object[]> data = new ArrayList<>();
for (JsonNode node : rootNode) {
String testCase = node.get("testCase").asText();
String fromType =
node.get("fromAccount").get("type").asText();
String toNumber =
node.get("toAccount").get("number").asText();
String amount = node.get("amount").asText();
String description =
node.get("description").asText();
String expectedResult =
node.get("expectedResult").asText();
String expectedMessage =
node.get("expectedMessage").asText();
data.add(new Object[] {
testCase, fromType, toNumber, amount,
description, expectedResult, expectedMessage
});
}
return data.toArray(new Object[0][]);
}
@Test(dataProvider = "transferData")
public void testBankingTransfer(
String testCase, String fromType,
String toNumber, String amount,
String description, String expectedResult,
String expectedMessage) {
navigateTo("/banking");
LoginPage login = new LoginPage(driver);
DashboardPage dashboard =
login.loginAs("testuser", "password123");
TransferPage transfer = dashboard.clickTransfer();
transfer.selectFromAccount(fromType);
transfer.enterToAccount(toNumber);
transfer.enterAmount(amount);
transfer.enterDescription(description);
transfer.clickTransfer();
if ("success".equals(expectedResult)) {
Assert.assertTrue(
transfer.isConfirmationDisplayed(),
"Transfer should succeed for: " + testCase);
Assert.assertEquals(
transfer.getConfirmationMessage(),
expectedMessage);
} else {
Assert.assertTrue(
transfer.isErrorDisplayed(),
"Transfer should fail for: " + testCase);
Assert.assertEquals(
transfer.getErrorMessage(),
expectedMessage);
}
}
}Instead of navigating JsonNode manually, you can map JSON directly to Java objects. This gives you compile-time type safety.
// Step 1: Create a POJO matching the JSON structure
public class TransferTestData {
private String testCase;
private Account fromAccount;
private Account toAccount;
private String amount;
private String description;
private String expectedResult;
private String expectedMessage;
// Standard getters and setters
public String getTestCase() { return testCase; }
public void setTestCase(String testCase) {
this.testCase = testCase;
}
public Account getFromAccount() { return fromAccount; }
public void setFromAccount(Account fromAccount) {
this.fromAccount = fromAccount;
}
public Account getToAccount() { return toAccount; }
public void setToAccount(Account toAccount) {
this.toAccount = toAccount;
}
public String getAmount() { return amount; }
public void setAmount(String amount) {
this.amount = amount;
}
public String getDescription() { return description; }
public void setDescription(String description) {
this.description = description;
}
public String getExpectedResult() { return expectedResult; }
public void setExpectedResult(String expectedResult) {
this.expectedResult = expectedResult;
}
public String getExpectedMessage() {
return expectedMessage;
}
public void setExpectedMessage(String expectedMessage) {
this.expectedMessage = expectedMessage;
}
}
public class Account {
private String type;
private String number;
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getNumber() { return number; }
public void setNumber(String number) {
this.number = number;
}
}
// Step 2: Read JSON into POJO array
@DataProvider(name = "transferDataPojo")
public Object[][] readTransferDataPojo() throws IOException {
ObjectMapper mapper = new ObjectMapper();
TransferTestData[] testData = mapper.readValue(
new File("src/test/resources/test-data/"
+ "transfer-data.json"),
TransferTestData[].class);
Object[][] data = new Object[testData.length][1];
for (int i = 0; i < testData.length; i++) {
data[i][0] = testData[i];
}
return data;
}
// Step 3: Test method receives the entire object
@Test(dataProvider = "transferDataPojo")
public void testTransfer(TransferTestData td) {
// Access nested data with type safety
transfer.selectFromAccount(
td.getFromAccount().getType());
transfer.enterToAccount(
td.getToAccount().getNumber());
transfer.enterAmount(td.getAmount());
// ...
}| JsonNode Approach | POJO Approach |
|---|---|
| Quick to set up — no POJO classes needed | Requires creating Java classes matching JSON |
| Good for simple flat JSON | Best for complex nested JSON |
| No compile-time type checking | Full compile-time type safety |
| Easy to break with typos in field names | IDE auto-complete works on field names |
| Good for prototyping and small projects | Best for production frameworks |
Q: When would you use JSON instead of CSV or Excel for test data?
A: I use JSON when the test data has a nested or hierarchical structure — like a bank transfer with nested account objects. CSV and Excel are flat (rows and columns), so nested data gets awkward. JSON is also the natural format for API testing since REST APIs use JSON. I parse it using Jackson ObjectMapper, either into JsonNode for simple cases or into POJO classes for complex structures with full type safety.
Key Point: Use JSON when test data is nested. Jackson ObjectMapper parses it into JsonNode or POJOs.