Lombok tricks and common mistakes

Photo by Mervyn Chan on Unsplash

Project Lombok is a Java library which can generate some commonly used code and facilitate keeping source code clean, e.g. by using some annotations you can generate constructors, getters, setters and other helpful code for your classes.

In this post I will show you a few common use cases of Lombok, problems you may face and propose solutions for them.

Builders

Sometimes you need to have a builder method for your class with different arguments. E.g.:

@Value
class Pojo {

String username;
String surname;

@Builder
Pojo(User user, String surname) {
this.username = user.getEmail();
this.surname = surname;
}
}

As a side note: you don’t need to add private final on fields, because Lombok will generate them. This is a common mistake.

Then if you use toBuilder property of Lombok @Builder annotation you may face some tricky compilation errors which are hard to spot in source class because they are actually coming from Lombok generated sources.
E.g, try this code:

import lombok.Builder;
import lombok.Value;

@Value
class Pojo {

String username;
String surname;

@Builder(toBuilder = true)
Pojo(User user, String surname) {
this.username = user.getEmail();
this.surname = surname;
}
}

Compilation will fail with error:

Error:java: cannot find symbol
symbol: variable user

This is because Lombok generates the toBuilder() method with class fields like that:

public Pojo.PojoBuilder toBuilder() {
return (new Pojo.PojoBuilder()).user(this.user).surname(this.surname);
}

Obviously, this.user causes the compilation error.
To fix it you could add another @Builder annotation building from all class fields and set toBuilder property on it like that:

import lombok.Builder;
import lombok.Value;

@Value
class Pojo {

String username;
String surname;

@Builder(toBuilder = true)
Pojo(String username, String surname) {
this.username = username;
this.surname = surname;
}


@Builder
Pojo(User user, String surname) {
this.username = user.getEmail();
this.surname = surname;
}
}

Looks like now compilation succeeds and you can use the toBuilder() method.

But wait.. it's still wrong. Try to build an object with the new builder method and check which values were set:

Pojo pojo = Pojo.builder()
.user( new User("email@test.com"))
.surname("surname")
.build();

System.out.println(pojo.getUsername());
System.out.println(pojo.getSurname());

It prints:

null
surname

The generated builder() method is using the second builder (the one with toBuilder property) and not setting the usernamefrom passed User object. Lombok generated such code:

public static Pojo.PojoBuilder builder() {
return new Pojo.PojoBuilder();
}

public Pojo.PojoBuilder toBuilder() {
return (new Pojo.PojoBuilder()).username(this.username).surname(this.surname);
}

How to fix this properly?
We have to specify method and class name for the additional builder. This is the final working solution:

import lombok.Builder;
import lombok.Value;

@Value
class Pojo {

String username;
String surname;

@Builder(toBuilder=true)
Pojo(String username, String surname) {
this.username = username;
this.surname = surname;
}

@Builder(builderMethodName = "builderFromUser", builderClassName = "FromUserBuilder")
Pojo(User user, String surname) {
this.username = user.getEmail();
this.surname = surname;
}
}

Inheritance

Let's assume we have classes Parent and Child which is extending the Parent.

In order to be able to extend class we cannot use the @Value annotation as it makes the class final.
We can use @Data instead which will also generate getters, but we will have to implement a constructor.
Lombok is not able to generate constructor using inheritance information.

The implementation would look as follows:

import lombok.Builder;
import lombok.Data;
import lombok.Value;

@Data
class Parent {
private final String a;
}

@Value
class Child extends Parent {

String b;

Child(String a, String b) {
super(a);
this.b = b;
}
}

On the subclass we can use the @Value unless we plan to extend from this class as well.

After adding the required constructor the class compiles but with a warning:

Warning:(12, 1) java: Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.

So we should add the annotation on subclass:

@EqualsAndHashCode(callSuper=true)

Jackson serialisation and deserialisation

When implementing data transfer objects (DTOs) for use with Jackson library it is handful to use Lombok to eliminate the getters-setters boilerplate code, e.g.:

@Value
class ValueObject {

String login;
int age;
}

Then we could deserialize JSON like this:

ObjectMapper objectMapper = new ObjectMapper();
String json = "{\"login\" : \"johnsmith\", \"age\": 77}";
ValueObject vo = objectMapper.readValue(json, ValueObject.class);

But at runtime it fails:

om.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `lombok.tricks.jackson.ValueObject` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"login" : "johnsmith", "age": 77}"; line: 1, column: 2]

This is because the generated class looks as follows:

final class ValueObject {
private final String login;
private final int age;

public ValueObject(String login, int age) {
this.login = login;
this.age = age;
}

public String getLogin() {
return this.login;
}

public int getAge() {
return this.age;
}

// equals, hashCode and toString follow..
}

Jackson does not recognise that the constructor is a creator of the class, it expects default constructor or constructor annotated with: 
@JsonCreator e.g.:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

...

@JsonCreator
ValueObject(@JsonProperty("login") String login,@JsonProperty("age") int age) {
this.login = login;
this.age = age;
}

or with java beans annotation:

@ConstructorProperties({"login", "age"})
ValueObject(String login, int age) {
this.login = login;
this.age = age;
}

which is slightly shorter but still easy to make a mistake and rename argument name in constructor but not in annotation.

Fortunately, Lombok can generate it as well if we only configure it properly.
To do so, we have to add a property file in project root folder named lombok.config with content: 

lombok.anyConstructor.addConstructorProperties=true

Then we can be happy with clean DTO implementation like we initially wanted to have:

@Value
class ValueObject {

String login;
int age;
}

It works fine with Jackson now.

You can find examples of codes mentioned above on Github.

Hope you find this information useful and won’t do similar mistakes.