New versions of Jilt released
I first introduced Jilt, my Java library for generating Builder pattern classes, in an article on this blog back in 2017. And while it has been downloaded thousands of times since then, it has received only minor updates during that time, like making it work with Java 9+. The last release containing any functional changes happened in 2018 – six years ago.
However, a recent Tweet from Maciej Walkowiak, of Spring Cloud fame, and the subsequent activity around it, has resulted in a surge of interest in Jilt, and a few issues raised by its new customers. Given the uptick in activity, I figured it would be a good idea to address those issues in the library, and actually add some functionality to it for the first time in six years.
So, let’s discuss all the changes included in
Jilt’s newest release, 1.4
.
Support for record
Builders
The first problem
was that placing the @Builder
annotation directly on record
declarations
(introduced in Java 14)
did not work.
So, the following code:
import org.jilt.Builder;
@Builder
public record Person(String firstName,
String lastName, LocalDate dateOfBirth) {
}
Would result in the following error reported by Jilt during compilation:
error: @Builder can only be placed on classes, constructors or static methods
As it turns out, the fix was just a tiny adjustment in the setup code – all the code generation logic works the same way for records as it does for classes, so no changes were needed to be made there.
Thanks to Maciej Walkowiak for reporting this issue.
(Technically speaking, this change was included in the previous release,
1.3
, but since it was the only change in that version,
I felt it didn’t really deserve a separate article)
Terminology changes
Historically, I’ve always used the name “Type-Safe”
to refer to the Builder variant that ensures each required property
has been provided before constructing the instance of the target class.
However, looking at the above Twitter thread,
it’s clear that the Java community has settled on the name “Staged” for this concept.
Since a user reported this as an issue,
I figured it’s better to be consistent with the widely-adopter nomenclature,
and so I’ve added a new constant to the BuilderStyle
enum, called STAGED
.
In addition, I also took this opportunity to clarify the advantages of the TYPE_SAFE_UNGROUPED_OPTIONALS
style.
I think I didn’t do a good enough job explaining under which circumstances you would use it over STAGED
,
and the name I’ve originally chosen for it did not help with that.
So, I’ve also added a new value to the BuilderStyle
enum with the name STAGED_PRESERVING_ORDER
as a replacement for TYPE_SAFE_UNGROUPED_OPTIONALS
,
and updated its documentation to better explain its advantages
(mainly, that it allows changing a required property to optional without breaking existing code that used the previously-generated Builder).
Note that this change was made in a backwards-compatible way:
only new values were added to the enum,
the existing TYPE_SAFE
and TYPE_SAFE_UNGROUPED_OPTIONALS
values have not been modified,
their behavior has remained exactly as it was before,
and they have not been deprecated.
Thanks to vprudnikov for reporting the issue.
Problems with generic classes
As it turns out, Jilt could not be used for generic
(that is, taking type parameters) classes –
the resulting Builder would not preserve the type parameters
(it would use them in the return type of the build()
method,
but it would never declare them, so the generated code wouldn’t compile).
The original issue was reported back in 2022, but I lost track of it, since the original requester said he would provide an reproduction, but never did. However, I found it while working on the other issues mentioned in this article, and decided it was worth fixing as well.
So, for a class such as:
import org.jilt.Builder;
@Builder
public final class Wrapper<T> {
public final T item;
public Wrapper(T item) {
this.item = item;
}
}
Jilt will now generate the following Builder:
@Generated("Jilt-1.4")
public class WrapperBuilder<T> {
private T item;
public static <T> WrapperBuilder<T> wrapper() {
return new WrapperBuilder<T>();
}
public WrapperBuilder<T> item(T item) {
this.item = item;
return this;
}
public Wrapper<T> build() {
return new Wrapper<T>(item);
}
}
Interestingly, this issue also surfaced a bug in JavaPoet, the library Jilt depends on for code generation. When generating a Staged Builder for a class with type parameters, there’s an edge case where the return type in the method declaration of the interfaces generated for making sure the given required property has been set is shadowed by the type parameter. This happens when the name of the field or parameter the property is derived from is the same (after uppercasing its first letter) as the name of the type parameter.
Here’s an example of a class that demonstrates this problem:
import org.jilt.Builder;
import org.jilt.BuilderStyle;
@Builder(style = BuilderStyle.STAGED)
public final class Pair<First, Second> {
public final First first;
public final Second second;
public Pair(First first, Second second) {
this.first = first;
this.second = second;
}
}
This generates the following per-property interfaces:
@Generated("Jilt-1.4")
public interface PairBuilders {
interface First<First, Second> {
Second<First, Second> first(First first);
}
interface Second<First, Second> {
Optionals<First, Second> second(Second second);
}
interface Optionals<First, Second> {
Pair<First, Second> build();
}
}
The issue here is that the Second
in the return type of the first()
method in the First
interface refers to the type parameter,
which shadows the next interface in the chain.
The correct thing here would be to qualify the type name with the name of the enclosing interface,
so JavaPoet should generate the return type as PairBuilders.Second
.
I’ve opened a bug about it to the project.
Thanks to Thibault Urien and Giacomo Baso for reporting and commenting on the issue.
@Nullable
annotation should make properties optional
A recent trend that’s gaining popularity in the Java ecosystem is using annotations to denote whether a given variable,
field or parameter is permitted to be null
.
Some people like to use the Optional
type,
introduced in Java 8,
for that purpose,
but the problem with doing that is the fact that an Optional
instance can still be null
itself,
and additionally, Java maintainers like Brian Goetz have clearly said that
this is not the intended way to use that type.
Typically, you would use the annotation @Nullable
to denote that a given value can be null
,
and the annotation @NonNull
(or @NotNull
) that it cannot be null
(many projects also follow the convention that not specifying any annotation implies the value cannot be null
,
which makes the code significantly less verbose).
This allows IDEs and build tools to emit warnings or errors when violating these annotation-based rules
(for example, passing the result of a method with the return type annotated as @Nullable
into a parameter annotated with @NonNull
).
This is relevant for Jilt, since null
is the default value for optional properties of reference types
if a value has not been provided for them before constructing an instance of the target class.
So, if a given field or property is marked as @Nullable
, it’s natural to treat it as an optional property.
However, before the 1.4
release, Jilt had no awareness of @Nullable
annotations,
and thus you always had to add the @Opt
annotation explicitly in order to make the property optional,
which could get tedious if a class had many properties.
In 1.4
, I’ve decided to change this behavior, and now any property generated from a field or
constructor/static method parameter annotated with @Nullable
is automatically considered optional,
without having to explicitly add the @Opt
annotation to it.
The tricky part about this is that there is not a single set of these annotations that are widely considered standard in the Java community.
Instead, the situation is more similar to logging, where you have multiple competing alternatives,
like SLF4J,
Log4J2,
and others.
In the case of null
-permitting annotations, you have JSR-305,
JetBrains annotations,
and jspecify
,
just to mention a few.
However, in a stroke of luck, all of them use the same name, @Nullable
,
for the null
-permitting annotation.
So, in Jilt, we simply respect any annotation with the name Nullable
,
regardless of what package it belongs to.
This means that, in theory, you could define your own @Nullable
annotation if you wanted to,
and Jilt would recognize it, and automatically make the property annotated with it optional!
In addition to making the property optional, Jilt also propagates the @Nullable
annotation to the generated setter
methods for that property, to make sure the IDEs and build tools don’t report false positives when passing values to them.
Thanks (again) to Maciej Walkowiak for reporting the issue.
Summary
So, those are all the changes included in Jilt release 1.4
.
I’d love any feedback you might have about these, and the library in general.