The previous three lessons built POJOs the long way — explicit constructors, getters, setters, and the occasional equals/hashCode. That's roughly thirty lines of ceremony for a five-field model, repeated for every class. Lombok is a small annotation library that generates all of that at compile time. One annotation produces every getter and setter; another produces a fluent builder. The Java code shrinks, the wire format stays identical, and Jackson doesn't notice the difference. This lesson is the small set of Lombok annotations that earn their keep in a Rest Assured suite.
Adding Lombok to the project
The dependency goes in pom.xml with <scope>provided</scope> — Lombok runs at compile time, not in production:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>For IntelliJ: install the official Lombok plugin (Settings → Plugins → search "Lombok"). For VS Code: install the Lombok Annotations Support for VS Code extension. Eclipse needs a one-time lombok.jar -runInstaller step. The plugin teaches the IDE that @Data-annotated classes have getters and setters at compile time, even though the source file doesn't show them. Without it, the IDE shows phantom errors on every user.getName() call.
Before Lombok
A representative POJO from the previous lesson, in full:
package com.mycompany.apitests.models.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.Objects;
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResponse {
private int id;
private String name;
private String email;
private String role;
private String createdAt;
public UserResponse() {}
public UserResponse(int id, String name, String email, String role, String createdAt) {
this.id = id;
this.name = name;
this.email = email;
this.role = role;
this.createdAt = createdAt;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }
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; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String c) { this.createdAt = c; }
@Override public String toString() {
return "UserResponse{id=" + id + ", name='" + name + "', ...}";
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserResponse u)) return false;
return id == u.id && Objects.equals(name, u.name)
&& Objects.equals(email, u.email); // ... etc
}
@Override public int hashCode() {
return Objects.hash(id, name, email, role, createdAt);
}
}Five fields, ~30 lines of pure ceremony. None of it is interesting; all of it is required for Jackson, debugging, and assertEquals to behave.
After Lombok
Same class, same wire format:
package com.mycompany.apitests.models.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResponse {
private int id;
private String name;
private String email;
private String role;
private String createdAt;
}Five fields, five lines of fields, three Lombok annotations. Compile this and the resulting .class file has every getter, every setter, an empty constructor, an all-args constructor, toString, equals, and hashCode — exactly the previous version, byte for byte.
The annotations worth knowing
@Data— the everything bagel. Generates getters for every field, setters for every non-final field, atoString,equals, andhashCode. Use on POJOs that act like data containers. (Most of yours.)@Getter/@Setter— only one direction. Use when you want immutable-ish models (getters but no setters) or only the one direction. Can be applied per-field, too.@NoArgsConstructor—public UserResponse() {}. Required by Jackson for deserialisation; pair with@AllArgsConstructorfor tests that build instances inline.@AllArgsConstructor— constructor taking every field. Convenient fornew UserResponse(1, "Alice", "alice@test.com", ...)in tests.@RequiredArgsConstructor— constructor taking everyfinaland@NonNullfield. Useful when only some fields are required at construction.@ToString— onlytoString(without the rest of@Data). When you want the debug output but not the equals/hashCode.@ToString(exclude = "password")to keep secrets out of logs.@EqualsAndHashCode— equality without setters/getters. Sameexcludeoption works.
The @Data + @NoArgsConstructor + @AllArgsConstructor trio covers ~80% of test POJOs. Reach for the more surgical annotations only when @Data's defaults don't fit.
@Builder — the test data superpower
@Builder generates a fluent builder for the class:
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class CreateUserRequest {
private String name;
private String email;
@Builder.Default
private String role = "tester";
@Builder.Default
private boolean active = true;
}Usage:
CreateUserRequest user = CreateUserRequest.builder()
.name("Alice")
.email("alice@test.com")
.role("admin")
.build();
// Defaults kick in when fields are omitted
CreateUserRequest viewer = CreateUserRequest.builder()
.name("Bob")
.email("bob@test.com")
.build(); // role = "tester", active = true via @Builder.DefaultWhy this matters in tests: most test data has a few "interesting" fields and a sea of default-able ones. Builders let each test name only the fields that matter to it, leaving the rest implicit. A randomUser() factory method becomes one line:
public static CreateUserRequest randomUser() {
return CreateUserRequest.builder()
.name("Test User " + UUID.randomUUID())
.email("test+" + UUID.randomUUID() + "@test.com")
.build();
}@Builder.Default is the small detail that catches many newcomers — without it, default values on fields are ignored by the builder. Always pair the two.
Lombok and Jackson — quietly compatible
Jackson serialises and deserialises via getters and setters, the bean convention. Lombok generates getters and setters that conform to the same convention. The two have no idea about each other; they cooperate transparently:
@Data @NoArgsConstructor @AllArgsConstructor
public class User {
private String name;
@JsonProperty("email_address")
private String email;
@JsonIgnore
private String internalNotes;
}All Jackson annotations work as before. Lombok produces the methods Jackson reads. There's no extra wiring.
A complete request POJO with Lombok
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UpdateUserRequest {
private String name;
private String email;
private String role;
}Eight lines. Equivalent to ~40 lines without Lombok, identical on the wire, identical to Jackson, identical to call sites that use .builder(). The improvement compounds: a project with 20 model classes saves hundreds of lines of mechanical code.
Lines saved at a glance
A 5-field POJO — same Jackson behaviour, very different source
Without Lombok
~30 lines for 5 fields
Manual getters / setters
Manual constructors (no-args + all-args)
Manual toString / equals / hashCode
Boilerplate scales linearly with field count
Adding a field = editing 4-5 places
With Lombok
~10 lines for 5 fields
@Data: getters + setters + toString + equals + hashCode
@NoArgsConstructor + @AllArgsConstructor
@Builder: fluent test data construction
Source size constant per class
Adding a field = adding the field
Beyond line count: the Lombok version is easier to review. A change that adds a field is one line of diff, not five. A change that renames a field is one rename, not five.
A note for TypeScript-trained eyes
TypeScript for QA does this without help — interface User { name: string; email: string; } is the entire equivalent. Java's verbosity is real and Lombok exists because the language doesn't have first-class data types. (Java records, introduced in Java 14, do something similar — but they're immutable, which collides with Jackson's setter-driven deserialisation. For request/response POJOs, Lombok still wins.)
⚠️ Common mistakes
- Forgetting the IDE plugin. Without it, the IDE flags every
user.getName()as a missing method. Tests compile and pass on the command line, but the editor experience becomes unusable. Install the plugin for every IDE the team uses. - Forgetting
@Builder.Default. A field initialised inline (private String role = "tester") is ignored by@Builder. Without@Builder.Default, the generated builder leaves it null. Add the annotation to every default-bearing field. - Using
@Dataon a class with circular references.@DatageneratestoString,equals, andhashCodethat recurse into every field. A class graph with a cycle (User → Order → User) blows the stack. Either annotate one side with@ToString.Excludeand@EqualsAndHashCode.Exclude, or break the cycle.
🎯 Practice task
Convert your existing POJOs to Lombok and feel the line-count difference. 20–30 minutes.
- Add the Lombok dependency to
pom.xml(<scope>provided</scope>). Install the IDE plugin. Runmvn clean compileand confirm the build succeeds. - Pick the largest POJO from Lesson 3 (
UserResponse, probably). Replace the constructors, getters, setters,toString,equals,hashCodewith@Data @NoArgsConstructor @AllArgsConstructor. Run the tests against it — they should all pass without changes. - Use
@Builderfor a test factory. AnnotateCreateUserRequestwith@Data @Builder. Write a static helperTestData.randomUser()returningCreateUserRequest.builder().name("...").email("...").build(). Rewrite three tests to use it. @Builder.Default. Add a defaultrole = "tester"field onCreateUserRequest. Build a request without settingroleand confirm via.log().body()that"role": "tester"is on the wire.- Selective annotations. Take an
ErrorResponsemodel. Use@Getteronly (no setters — error responses are immutable from the test's perspective). Confirm Jackson can still deserialise into it. (It can — Jackson'ssetterlessdeserialisation finds the constructor.) @ToString.Excludeon a sensitive field. Add apasswordfield to a request POJO and annotate with@ToString.Exclude. Print an instance — confirm the password is omitted from the output. This prevents secrets in logs.- Diff the LOC. Run
git diff --staton the model package. Note the negative number — the same behaviour, fewer lines. - Stretch: add
@Withto a model. It generates awithName(...)method that returns a copy of the object with one field changed. Useful for parameterised tests:baseUser.withRole("admin")produces a variant without mutating the original.
That's serialisation covered. Chapter 6 turns to framework architecture — how to lift the patterns from the last six chapters into a base test class, shared specs, filters, and helpers that scale across an entire test suite.