Java Records tortured with Lombok yet again (builder edition)
Last year I wrote two posts about torturing Java records with Lombok. Fortunately (or not, depends on your point of view), Lombok’s team decided to end these sick plays. So it seems that with Java 16 and Lombok 1.18.20 there’s no more tossing @Getter
or @With
on records. More details can be found in their GitHub issue #2578. Basically, javac generates errors like:
@ToString is only supported on a class or enum.
which makes
@ToString(of="f1")
public record RecordWithToStringFromLombok(int f1, String f2) {}
impossible. The same happens, if you annotate the record with e.g. @Getter
. It seems that records are not classes ;-) Or that Lombok feels uncomfortable generating toString()
from subset of record’s fields.
If you still desperately need getters for record’s components, not just accessors, you can go for something like this:
public record RecordWithGetters(@Getter int f1, @Getter String f2) {}
which won’t generate any errors, but it won’t generate any getters either. Boom. Add getters manually, apart from accessors, if your library/framework desperately needs the get
prefix. Or just stick to JavaBeans.
However, maybe there are still other ways to “exploit” Lombok with records and bring it to another level? Maybe this time it could be even somewhat useful?
Do Java records have builder?

Short answer: no. Currently, there’s no ‘default’ or ‘standard’ builder for records. If you need one, you can create one on your own. Or… we can misbehave and try to (ab)use Lombok yet another time. ;-)
Let’s say we have a dummy record like this:
record BuilderRecord(int a, String b, Double c) {}
and we’d like to create instances using builder, as it sounds more fluent. Adding @Builder
doesn’t show any errors. However, the only method generated in BuilderRecord.builder()
is build()
. Not quite what we expected.
Also, delomboking the @Builder
generates:
record BuilderRecord(int a, String b, Double c) {
BuilderRecord() {
}
public static BuilderRecordBuilder builder() {
return new BuilderRecordBuilder();
}
public static class BuilderRecordBuilder {
BuilderRecordBuilder() {
}
public BuilderRecord build() {
return new BuilderRecord();
}
public String toString() {
return "BuilderRecord.BuilderRecordBuilder()";
}
}
}
which doesn’t compile, because the constructor isn’t canonical. Oops.
However, we can keep pushing. After all, @Builder
is annotated @Target({TYPE, METHOD, CONSTRUCTOR})
… ;-)
Let’s create an empty compact constructor, only to annotate it with @Builder
:
@Builder
BuilderRecord {}
Bingo!
This time we can write:
var sample = BuilderRecord.builder().a(42).b("answer").c(Double.POSITIVE_INFINITY).build();
System.out.println(sample);
and it produces the expected output:
BuilderRecord[a=42, b=answer, c=Infinity]
Just for the sake of completeness, the delomboked version is as follows:
record BuilderRecord(int a, String b, Double c) {
BuilderRecord {
}
public static BuilderRecordBuilder builder() {
return new BuilderRecordBuilder();
}
public static class BuilderRecordBuilder {
private int a;
private String b;
private Double c;
BuilderRecordBuilder() {
}
public BuilderRecordBuilder a(int a) {
this.a = a;
return this;
}
public BuilderRecordBuilder b(String b) {
this.b = b;
return this;
}
public BuilderRecordBuilder c(Double c) {
this.c = c;
return this;
}
public BuilderRecord build() {
return new BuilderRecord(a, b, c);
}
public String toString() {
return "BuilderRecord.BuilderRecordBuilder(a=" + this.a + ", b=" + this.b + ", c=" + this.c + ")";
}
}
}
This time the delomboked code compiles and works the same way.
It seems then that maybe there’s a way to still toss Lombok onto a record. And maybe (just maybe) the result isn’t that bad…? It would be nicer, of course, if we didn’t have to use the compact constructor.
If you have more sick ideas about (mis)using records and Lombok, let me know!