The previous chapters built request bodies as Strings or Maps and asserted on responses field by field. That works, but it asks the test author to keep two parallel realities in their head — the JSON shape on the wire, and the Java code expressing it. Jackson's serialisation erases that gap: hand Rest Assured a typed Java object, and Jackson turns it into JSON automatically. Field renames become compile errors. Autocomplete suggests the right names. The shape lives in one place — the POJO. The Core Java for QA lesson on classes and Jackson set up the language; this lesson is how that translates into the Rest Assured request chain.
A POJO and a request
The simplest serialisation example. Define a class with the fields the API expects:
package com.mycompany.apitests.models;
public class User {
private String name;
private String email;
private String role;
public User(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}
// getters and setters
public String getName() { return name; }
public void setName(String n) { this.name = n; }
public String getEmail() { return email; }
public void setEmail(String e) { this.email = e; }
public String getRole() { return role; }
public void setRole(String r) { this.role = r; }
}Pass an instance to body():
@Test
public void createUserFromPojo() {
User user = new User("Alice", "alice@test.com", "admin");
given()
.contentType(ContentType.JSON)
.body(user)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice"));
}Rest Assured detects Jackson on the classpath (the Chapter 1 pom.xml already pulled it in transitively) and uses it to serialise the object. The wire payload comes out as:
{ "name": "Alice", "email": "alice@test.com", "role": "admin" }No string-building, no escape characters, no chance of a misspelled key — the field name is the field name, enforced by the compiler.
Why Jackson uses getters, not fields
Jackson reads the object's getters by default, not the private fields directly. getName() produces the JSON key name (the Java bean convention strips get and lowercases the first letter). This is why your POJOs need getters even when they look redundant — without them, Jackson sees an empty object and serialises {}. The same convention applies in reverse for deserialisation, which the next lesson covers in detail.
Controlling the JSON output
Real APIs rarely accept a one-to-one mapping between Java and JSON conventions. Jackson's annotations bridge the gap.
@JsonProperty — rename a field on the wire:
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
@JsonProperty("full_name") // JSON: "full_name"; Java: name
private String name;
@JsonProperty("email_address")
private String email;
// getters/setters omitted
}The Java code uses user.getName(); the wire payload uses "full_name": "...". Common when the API team uses snake_case but the team prefers Java's camelCase, or when the API has its own legacy naming.
@JsonIgnore — keep a field out of the JSON:
public class User {
private String name;
@JsonIgnore
private String internalDebugId; // never sent to the API
}Useful for fields the test code carries for its own bookkeeping (audit IDs, internal flags) that the API has no opinion about.
@JsonInclude(NON_NULL) — drop nulls:
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateUserRequest {
private String name;
private String email;
private String role; // optional
private String department; // optional
}Without it, an unset department serialises as "department": null, which a strict API may reject. With NON_NULL, missing fields are silently dropped — which is usually what partial update and optional field APIs want.
@JsonFormat — control date formatting:
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
public class Event {
@JsonFormat(pattern = "yyyy-MM-dd")
private Date eventDate; // → "2024-01-15"
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
private Date createdAt; // → "2024-01-15T10:00:00Z"
}Different APIs use different date formats. Pin yours per field; don't rely on Jackson's default (which serialises Date as a Unix epoch).
Nested POJOs serialise transparently
Jackson recurses into fields whose types are also POJOs:
public class Address {
private String street;
private String city;
private String postcode;
// getters/setters
}
public class User {
private String name;
private String email;
private Address address; // nested
// getters/setters
}
User user = new User("Alice", "alice@test.com",
new Address("221B Baker St", "London", "NW1 6XE"));Serialises to:
{
"name": "Alice",
"email": "alice@test.com",
"address": {
"street": "221B Baker St",
"city": "London",
"postcode": "NW1 6XE"
}
}The same goes for List<T>, Map<String, T>, and any other Jackson-aware type. The shape of the POJO graph mirrors the shape of the JSON.
Serialisation visualised
Step 1 of 5
Build the POJO
new User("Alice", "alice@test.com", "admin") — a normal typed Java object built in the test.
The compile-time wins are real and constant: rename email to emailAddress once on the POJO, every test using it updates automatically. Try the same with hand-written JSON strings and you're searching the codebase for "email".
Serialising a list of POJOs
For endpoints that take an array body — a bulk create, a batch update — pass a List:
List<User> bulk = List.of(
new User("Alice", "alice@test.com", "admin"),
new User("Bob", "bob@test.com", "viewer"),
new User("Carol", "carol@test.com", "tester")
);
given()
.contentType(ContentType.JSON)
.body(bulk)
.when()
.post("/users/bulk")
.then()
.statusCode(201);Jackson serialises the list as a JSON array of objects. Same for Maps (serialised as JSON objects), Set (serialised as JSON arrays — order not preserved), and Optional (Optional.empty() → null).
Why this beats Map and String
Three concrete wins, repeated daily:
- Refactor safety.
user.setEmial(...)is a compile error.body.put("emial", ...)is a runtime 400. Multiply across 50 tests. - Autocomplete. Typing
user.in the IDE lists every field with its type. Hand-written JSON has no support beyond a JSON-aware syntax checker. - Reuse. The same
UserPOJO doubles as the response model —extract().as(User.class)gives you a typed object back. The next lesson is about that direction.
The trade-off is the upfront class definition. For a body used in three or more tests, the trade is overwhelmingly worth it; for a one-off, a Map still has its place.
⚠️ Common mistakes
- Forgetting getters. Jackson serialises via the bean property convention — get-prefixed methods. A POJO with private fields and no getters serialises as
{}. Either generate getters with the IDE, use Lombok (Lesson 4), or use Jackson's@JsonAutoDetect(fieldVisibility=ANY)to opt in to direct field access (less common, more invasive). - Forgetting a no-args constructor. Serialisation alone doesn't need it, but the response deserialisation in the next lesson does. Add
public User() {}from day one — Jackson can't construct a User without it. - Mixing snake_case and camelCase silently. A POJO with
firstNameagainst an API that expectsfirst_nameproduces a wire payload the API rejects with a confusing 400. Either annotate every field with@JsonPropertyor set Jackson'sPropertyNamingStrategy.SNAKE_CASEglobally — but pick one and stick to it.
🎯 Practice task
Build a User POJO and exercise serialisation against REQRES. 25–30 minutes.
- Create
src/test/java/com/mycompany/apitests/models/User.javawith three fields (name,job, optionalid), a no-args constructor, an all-args constructor, and getters/setters. Compile. - Write
createUserFromPojo()— POST/api/userswith aUserinstance, assert 201 and that the response includes the name you sent. @JsonProperty. Rename the Java fieldnametofullNameand add@JsonProperty("name"). Re-run — the test should still pass because the wire format hasn't changed.@JsonIgnore. Add a fieldinternalNotesto your User class. Annotate with@JsonIgnore. Set it on the instance, POST, and confirm the request body (via.log().body()) does not includeinternalNotes.@JsonInclude(NON_NULL). Add a nullable fieldphone. POST a User without settingphone. Without the annotation, the body contains"phone": null; add the annotation and confirm the field disappears from the wire.- Nested object. Define an
Addressclass. Add a fieldAddress addresstoUser. POST a User with a populated Address and confirm the JSON shape is{ ..., "address": { ... } }. - List body. POST a
List<User>to/api/users(REQRES happily echoes whatever shape you send). Use.log().body()to confirm the wire payload is a JSON array. - Stretch: add a field
Date createdAtand@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss'Z'", timezone="UTC"). POST and confirm the wire payload uses ISO 8601, not the Unix epoch fallback.
Next lesson: the other direction — taking the JSON the API sends back and turning it into a typed Java object you can assert on with normal getters.