Java Bean Validation is an Anti-Pattern
Author
Marcus HeldThe javax.validation
package is widely used in our industry. And I don’t like it. I believe using bean validation is
an anti-pattern. It hides business relevant constraints, it leaves the choice when a validation happens to other
framework code, and I even saw cases where developers expected that the validation “just had to take place”, but it
never happened. Of course, there was also no test for it. And speaking about tests - testing these business relevant
constraints is painful as well.
In a Spring/JPA application, I recently saw code like this:
@Entity
class AppUser(
@Email
var email: String,
) {
@Id
val id: UUID = UUID.randomUUID()
}
The business constraint is that only valid email addresses are allowed for the AppUser
. The @Email
annotation is
part of the javax.validation
package, and it instructs the validator to check if the property is a valid email
address.
But when we run this code, no exception is thrown:
@Service
class AppUserService(private val userRepository: AppUserRepository) {
@EventListener(ApplicationStartedEvent::class)
fun createUser() {
userRepository.save(AppUser("This is not an email address"))
}
}
So, we end up with a business requirement - that only valid e-mail addressees are supposed to be stored - and as a reader of the code we also expect that this is the case, but actually nothing happens. And of course there’s no test to check if that requirement is fulfilled.
Testing is hard
Speaking about tests - testing it is hard. Since you move the responsibility to validate your business requirements to " some" part of the framework, you can’t write a unit test. You must write an integration test to check if your validation does what you expect. This is bad for many reasons. Especially since integration tests are heavy. And booting up your application for such a small requirement is an overkill.
Kotlin and Bean Validation
But I didn’t answer why above check is not performed. The issue in combination with Kotlin is that we didn’t annotate
the backing property but the constructor parameter. javax.validation
does not pick this annotation up, and therefore
it is never validated. To fix that we actually need to annotate the property with @field:Email
.
In my opinion this example went from bad to worse. We did not protect our code from constructing the entity with invalid
data, we even built up the expectation of the reader that this can only contain valid email addresses, but that is
actually never checked. And of course we never test it.
All of above thought brings me to the conclusion:
Don’t use Java Bean Validation at all!
The JSR is unnecessary. Validation is nothing we should hide. We validate our data because it checks for certain
business constraints. This is business code. It belongs into the main flow of our application. We should not move it to
some magic library and rely on the framework. We also not even save much by using these annotations. Most of the checks
are super easy if
statements. And this is exactly what it should be!
Instead, do your validations in the constructor. Your code must not allow to construct an object with invalid data. So above entity I’d remodel like this:
@Entity
class AppUser(
var email: String,
) {
@Id
val id: UUID = UUID.randomUUID()
init {
if (!EmailValidator.isValid(email)) {
throw IllegalStateException("Email address is invalid")
}
}
}
Note “Even cleaner: Kotlins check function” In the Kotlin standard library exists a
check
function. Using it over a simpleif
has some benefits. E.g. it evaluates the provided message lazily and just uses a single line instead of three. So, above code can be written as:init { check(!EmailValidator.isValid(email)) { "Email address is invalid" } }
It is easy to read, simple and - from a runtime view - unambiguous. Writing a unit test for this constraint is a no-brainer as well. Java Bean Validation is an anti-pattern!