JSR-380, also known as Bean Validation 2.0, is a standard. Should we use it everywhere then? Let’s consider the pros and cons.
Note: This blog was originally posted on medium.com
What’s the story?
I’m working on a project with a couple of smart guys. We’ve been discussing how to validate our data. I opted for manual, custom validation, based on Vavr’s Validation. However, one of my teammates pointed out we could consider using Java Bean validation and its custom validators.
Well, why not to give it a shot, I thought. I decided to try it shortly and to describe my experiences here. I treat it as an interesting exercise and a possibility to look at the problem of data validation from a different perspective.
The domain
So, here is our data: bank account data holding IBAN, BIC codes and a bank account number. Before we store it in our database, we would like to be sure that all fields are valid. iban4j is the library that provides all the mechanics required in our example.
You can find all the snippets and the complete code on Github.
Validate it!
How to construct a custom validation? You can find a detailed tutorial in the Hibernate documentation. Here, I focus on the basics. Thus we need to implement only two things:
- an annotation to apply it to a field or a method,
- also, a validator mechanism to run data checks.
First, the annotation — let’s create one for the IBAN field. Here is how it can look like:
@Target({ ElementType.METHOD, ElementType.FIELD }) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Constraint(validatedBy = IbanValidator.class) | |
public @interface Iban { | |
String message() default "{iban.constraint}"; | |
Class<?>[] groups() default {}; | |
Class<? extends Payload>[] payload() default {}; | |
} |
What do we have here? First of all, we can apply the annotation to a field or a method. Next, a field annotated with the @Iban
is validated with IbanValidator
class. And at least, for every JSR-380 compliant annotation, we can configure:
- a message (in the example I have provided a reference to a localized message, contained in the
ValidationMessages.properties
file), - groups constraints (constraints can be grouped in subsets so that they can be applied to particular objects),
- also, a payload (defines additional data used by validators).
Here is the mentioned validator itself:
As you can see, it implements the ConstraintValidator
interface. When a value
is invalid, the validation method returns false
, otherwise true
.
If we are using the Spring Framework, we can run the validation automatically by applying the @Valid
annotation on a body of a request received through a REST endpoint.
The Framework applies the validation to every incoming request to a given endpoint. In a case when it fails we MethodArgumentNotValidException
We can run validation manually as well. It looks like the following:
BankAccount bankAccount = newBankAccount("invalid_iban", "invalid_bic", null); | |
final Set<ConstraintViolation<BankAccount>> constraints = | |
Validation.buildDefaultValidatorFactory().getValidator().validate(bankAccount); |
What has surprised me after calling the code above is the thrown exception:javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.softwaremill.jsr380.BankAccountValidator (…)
It turns out Spring and a default ValidatorFactory
instantiates instances of validators differently. The former does not require validators being public classes while the latter does. After changing the access modifier to validators, I could try them manually. Here is an example of a response for malformed bank account data:
ConstraintViolationImpl{ | |
interpolatedMessage='{iban.constraint}', propertyPath=iban, | |
rootBeanClass=class com.softwaremill.jsr380.domain.BankAccount, messageTemplate='{iban.constraint}' | |
} | |
ConstraintViolationImpl{ | |
interpolatedMessage='{bic.constraint}', propertyPath=bic, | |
rootBeanClass=class com.softwaremill.jsr380.domain.BankAccount, messageTemplate='{bic.constraint}' | |
} | |
ConstraintViolationImpl{ | |
interpolatedMessage='{bank-account-number.constraint}', propertyPath=bankAccountNumber, | |
rootBeanClass=class com.softwaremill.jsr380.domain.BankAccount, messageTemplate='{bank-account-number.constraint}' | |
} |
Ok, so does it work, right? Of course, it does!
But…
What I do not like here
In the example of running Bean Validation by hand, if I want to hide the validation mechanism inside the domain
package, I just simply can’t. I must expose validators classes to the whole world, so they are visible for instantiating. If you treat the validation as a part of a domain (like me), this can somewhat be frustrating that the standard pushes you to public things you would like to hide in a package.
Next, in the case of the Spring and validating data just before going into controllers methods, we don’t have to expose validators. As you can see, the behavior depends on the implementation of the validator factory.
However, in this situation, there is yet another drawback. With such a solution, I am validating a domain-specific data outside the domain package. For me, a controller is just a gateway giving access to the domain, not being a part of it.
Any thoughts?
If you are going to use some library or a framework or a standard, you probably have to adjust your code to a tool you have chosen. The Java Bean Validation case is not different. As usual, keep in mind that using standards not always solve all of your issues and pains. What’s more, it sometimes can cause more headaches than anticipated or different ones. So… choose wisely 🙂
It makes me rather a proponent of manual validation instead of using framework mechanisms. Why? A custom form of validation gives me full control over classes required in the process and over what happens when data is invalid. With such a solution, I can craft the exact shape of the data signaling error I need for my case. Moreover, I can hide classes inside the domain and publish them if there is a real requirement (e.g., I would like to reuse it somewhere else).
What I have found more, it is quite tricky to check one field in a context of the value of another field in a validated bean. For example, I would like to be sure that the provided bank account number is in line with the provided IBAN. It looks like this is doable with writing an annotation for a class instead of a field however, it does not look as clean as I would expect or I could achieve with custom code.
Also, Bean Validation uses annotations — if you didn’t know, they are considered harmful 😉
Did I miss something in this short tour that is valuable in Bean Validation? Do you have a different opinion or any other thoughts on the subject? Leave your comments below.
Photo by Lukas on stocksnap.io.
0 Comments