Adding Custom Validation in Lombok Builder

What is Lombok?

Lombok is a Java library that auto generates lots of boiler plate code for Java classes. Lombok works at compile time and manipulates Java byte code to add additional features. Lombok uses annotations to specify what boiler plate code will be generates. Eg. @Getter annotation can be applied to any Java bean to auto generate Getter methods for all the fields in that bean.

In this article we will be looking at, how to add custom validations on fields using Lombok Builder annotation

What is Builder annotation in Lombok?

Lombok can be used to generate Builder class for a Java bean, by annotating the bean class with @Builder annotation. 

In certain cases, we might want to validate the data, in builder class, before creating the bean object. Lombok provides simple validator annotations like @NonNull to enforce nullability checks. But what if, we want to have more custom validations or want to throw custom errors.

Customizing Lombok Builder class.

Let's consider below Employee class that requires custom format for `employeeId` field. Also, present in the class, is custom builder that performs validations in the `build()` method.

Employee.java

@Getter
@Builder
public class Employee {

    @NonNull
    private final String name;

    /**
     * Let's add following validations:
     * 1. Employee Id must start with `P` or `T`
     * 2. Must be of length 5
     */
    private final String employeeId;

    @NonNull
    private final LocalDate joiningDate;

    // ##########################################
    // Below we add the custom code to validate employeeId
    // ##########################################

    /**
     * Override the builder() method to return our custom builder instead of the Lombok generated builder class.
     *
     * @return
     */
    public static EmployeeBuilder builder() {
        return new CustomBuilder();
    }

    /**
     * Customized builder class, extends the Lombok generated builder class and overrides method implementations.
     */
    private static class CustomBuilder extends EmployeeBuilder {

        /**
         * Adding validations as part of build() method.
         *
         * @return
         */
        public Employee build() {

            if (super.employeeId == null || super.employeeId.trim().length() != 5) {
                throw new RuntimeException("Employee Id must be of 5 characters");
            }

            final char firstCharacter = super.employeeId.toCharArray()[0];
            if (firstCharacter != 'P' && firstCharacter != 'T') {
                throw new RuntimeException("Employee Id must begin with character `P` or `T`");
            }

            return super.build();
        }
    }
}

We can test the working of the above Employee class using below unit tests.

EmployeeTest.java

class EmployeeTest {

    /**
     * Create Employee object with valid data.
     */
    @Test
    public void valid_data() {
        Employee employee = Employee.builder()
                .name("James Wittel")
                .joiningDate(LocalDate.now())
                .employeeId("P1234")
                .build();

        Assertions.assertNotNull(employee);
    }

    /**
     * Creating employee object with null employee Id should generate error.
     */
    @Test
    public void null_employee_id() {
        Assertions.assertThrows(
                RuntimeException.class,
                () -> Employee.builder()
                        .name("James Wittel")
                        .joiningDate(LocalDate.now())
                        .employeeId(null)
                        .build());
    }

    /**
     * Creating employee object with invalid length employee Id should generate error.
     */
    @Test
    public void invalid_length_employee_id() {
        Assertions.assertThrows(
                RuntimeException.class,
                () -> Employee.builder()
                        .name("James Wittel")
                        .joiningDate(LocalDate.now())
                        .employeeId("P938838838")
                        .build());
    }

    /**
     * Creating employee object with invalid starting character of employee Id, should generate error.
     */
    @Test
    public void invalid_start_character_employee_id() {
        Assertions.assertThrows(
                RuntimeException.class,
                () -> Employee.builder()
                        .name("James Wittel")
                        .joiningDate(LocalDate.now())
                        .employeeId("A9388")
                        .build());
    }
}