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:
Keep the validation rules inside the class to avoid spreading the domain.
Avoid polluting the class with too much validation code for readability purposes.
Make the validation process compatible with the use of Lombok annotations or Java records.
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:
Using of Jakarta validations + Hibernate validator as a base to validate the beans.
Implementing a
SelfValidated
interface that will add a class the ability to exercise all its Jakarta annotations.
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);
}
}
}
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();
}
}
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();
}
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);
}
}
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 gotwith=~115ms
, andwithout=~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)