DEV Community

Cover image for Self-validated Java Objects
Diego Núñez Silva
Diego Núñez Silva

Posted on

Self-validated Java Objects

Problem

In Java there are several categories for objects whose function is to contain data, e.g. POJO, Java Bean, DTO, VO, entities, etc... They differ in some way, but their function is to carry data. For simplicity let's refer to them as Java objects.

In my experience, these classes/objects tend to be treated as second-class citizens because their function is simply to hold data, and no interesting stuff is included in them.

Further, developers often neglect unit-testing (serialization, deserialization, equals, etc...) of such classes/objects, but still, bugs can be introduced when their state is not properly validated. So, I tried to find a way of validating such objects while keeping the following goals in mind:

  1. Keep the validation rules inside the class to avoid spreading the domain.

  2. Avoid polluting the class with too much validation code for readability purposes.

  3. Make the validation process compatible with the use of Lombok annotations or Java records.

  4. Keep "compatibility" with Spring, so the validation method can be used in the same way as Spring's @Valid.

Implementation

The best solution I have found is inspired on Tom Hombergs' idea and consists of:

See the complete code in Github ...

Interface SelfValidated (code)

By making the Hibernate Validator instance static we ensure that a single instance will be created. A Validator is thread-safe and can be reused by all classes that implement SelfValidated.

/**
 * This interface allows a bean to exercise all the jakarta validations defined. All beans need to implement this interface,
 * and call validate() in order to be validated.
 */
public interface SelfValidated {

    /* NOTE: ValidatorFactory should be closed when the VM is shutting down (see App.java) */
    static final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    static final Validator validator = validatorFactory.getValidator();


    default void validate() {
        Set<ConstraintViolation<SelfValidated>> violations = validator.validate(this);

        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Java POJO Example + Lombok @Builder (code)

In the code below, the call to validate() could be moved to Builder's build() method, but I think a good convention would be to place it always in a constructor, as it will ensure that no instance will be created with an invalid state.

See unit-test

@Value
@Builder(builderClassName = "Builder", setterPrefix = "with")
@JsonDeserialize(builder = UserPojo.Builder.class)
public class UserPojo implements SelfValidated {

    @NotNull
    @Size(min = 8, max = 20)
    String username;
    @NotNull
    @Size(min = 8, max = 30)
    String password;

    private UserPojo(String username, String password) {
        this.username = username;
        this.password = password;
        validate();
    }
}
Enter fullscreen mode Exit fullscreen mode

Java Record (code)

Here we follow the same convention and make the call to validate() inside the constructor.

See unit-test

public record UserRecord(
        @NotNull
        @Size(min = 8, max = 20)
        String username,
        @NotNull
        @Size(min = 8, max = 30)
        String password,
        @NotNull
        @Email
        String email) implements SelfValidated {

    public UserRecord(final String username, final String password, final String email) {
        this.username = username;
        this.password = password;
        this.email = email;
        validate();
    }
Enter fullscreen mode Exit fullscreen mode

Spring Controller Example (code)

In the snippet below, @Valid is not actually necessary (left as documentation) because the instance will not even be created if the state is invalid. This means that Spring will not be the one executing the validation. Consequently, we need RestErrorHandler to tell Spring how do we want to handle the exception thrown by validate().

See unit-test

@RestController
@RequestMapping("/v1/test")
public class UserController {

    @PostMapping("user-pojo")
    public ResponseEntity<UserPojo> createUserPojo(@RequestBody @Valid UserPojo userPojo) {
        return ResponseEntity.ok(userPojo);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusions

From the original goals, I see:

Advantages

  • A guarantee that each object will have a valid state.

  • The beans encapsulate their validation rules so it is clear what a valid state is.

  • IMHO, the use of Jakarta validations makes the code very readable and it does not feel polluted.

  • Throwing ConstraintViolationException allows the catcher to identify violations detected in the bean. This works OK with Spring, but it changes a bit the usual behavior if compared with the use of @Valid + @RequestBody. It this case, the exception is different and it will be thrown even if @Valid is not given.

  • By adding intelligence to the bean we add a reason to create unit test to verify that everything works as expected.

Disadvantages

  • The approach works for immutable beans. For mutable ones, the method validate() needs to be called from outside the class, and that could be easily missed. A reason more to prefer immutability.
  • The beans suddenly take more importance and maintenance as they now include business logic, when traditionally they have been plain boring objects. This could lead to an overly strict/constrained use of such Java objects.
  • The use of Jakarta validations + Hibernate validator has a performance penalty. I tested creating 100 instances of UserPojo.java with and without validation and I got with=~115ms, and without=~5ms. These numbers were measured in a light Intel i5 laptop, but it gives an idea of the difference. In reality, validating a few objects in a real server it probably will not be noticeable, but it is good to keep it in mind.

Feedback

NOTE: Please let me know if you see any improvement, or have in general an opinion about the use of validations inside Java objects.

Thanks for reading!!

Image credits: Self-validated Java beans - Bing Image Creator

Top comments (0)