Why Lombok’s @Builder Drops Default Values and How to Fix It
This article walks through a real‑world NPE caused by Lombok’s @Builder ignoring field initializers, explains the underlying double‑initialisation bug, shows how @Builder.Default restores the defaults, and outlines Lombok’s compile‑time annotation processing mechanism.
Hello everyone, I’m SanYou. I recently ran into a puzzling Lombok issue and want to share the story.
I had a public service interface that had been stable for years and was handed over to another colleague. One day a new service tried to call the interface and threw a NullPointerException.
<code>if (reqDto.getField1()
&& reqDto.getField2() != null
&& reqDto.getField3() != null) {
// execute business logic when conditions are met
}
</code>The ReqDto class looks like this:
<code>@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReqDto {
private Boolean field1 = true;
private String field2;
private String field3;
}
</code>At first I assumed reqDto itself was null , but logging the request object showed it was not null, so the NPE must come from field1 .
Further investigation revealed two possible callers:
<code>ReqDto reqDto = new ReqDto();
reqDto.setField1(null);
</code>and
<code>ReqDto reqDto = ReqDto.builder()
.field2("why")
.field3("max")
.build();
</code>The builder version does not set field1 , relying on the default value. However, Lombok’s builder discards the initializer, leaving field1 as null and causing the NPE.
Adding the annotation @Builder.Default fixes the problem:
<code>@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReqDto {
@Builder.Default
private Boolean field1 = true;
private String field2;
private String field3;
}
</code>Why does Lombok need this extra annotation? The root cause is a double‑initialisation bug when @Builder and @NoArgsConstructor are used together. Lombok first initializes default values in the no‑args constructor, then the builder also tries to initialise them, leading to the field being overwritten with null . The Lombok team later merged the two steps, but the change broke the behaviour of plain new objects, so they marked the behaviour as a feature rather than a bug‑fix.
The discussion on GitHub (issue #1347) and the subsequent resolution in Lombok 1.18.2 (released July 2018) finally aligned the behaviour with the “least‑surprise” principle: objects created with new retain their field initializers, while builder‑created objects require an explicit @Builder.Default to keep them.
Beyond this specific bug, Lombok works via compile‑time annotations. It injects code during the javac compilation phase using an annotation processor that loads special SCL.lombok class files via Java’s SPI mechanism. The processor generates boilerplate such as getters, setters, and loggers (e.g., Log4j or SLF4J) based on the annotations present.
Understanding Lombok’s internals can help you avoid similar pitfalls and write more predictable Java code.
If you found this article helpful, please like, share, or bookmark it.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.