Validation
Before using a piece of a data you typically want to validate it first. Validation takes many forms and really depends on where the data is coming from and where the validation check is being done. If you’re passing data from one class to another via a function call then maybe some light validations are warranted (i.e., null checks) but if you’re receiving data from an external source you typically need to scrutinize it further.
Spring provides a light specification around validating Beans with a lot of the implementation provided by Hibernate.
Read: spring.io: Validation Read: hibernate.org: Hibernate Validators Read: tutorialspoint.com: Spring MVC - Hibernate Validator Example Read: mkyong.com: Combine Spring validator and Hibernate validator
The Trash/Error Bag Pattern
Although not an official design pattern Spring’s validation framework follows a technique called an error bag. The idea is named well because you basically create a “trash bag” object that you pass around to interested parties that will put their “trash” or errors into the bag. Afterwards you inspect the trash bag for errors and handle them accordingly.
- The trash bag is an instance of Errors.
- The interested parties your implementations of Validators.
The Validator Interface
The whole point of the Validator interface is to validate an object. Here is an example of a simple Person POJO.
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Person {
private String firstName;
private String lastName;
private String gender;
private Integer age;
}
I used a library here called Lombok that helps generate boiler plate code like getters, setters, hashcode, equals and toString.
We can create a Validator to validate a Person given whatever business requirements we have. For the purpose of brevity I’m only going to show one validation here. You can view the entire UserValidator.java in the Journey Through Spring exercise.
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Person person = (Person) target;
if(person.getAge() != null && person.getAge() < 21){
errors.rejectValue(
// the first parameter is the name of the field we are validating, reflection
// will be used to get it's value.
"age",
// the second parameter is an arbitrary errorcode that I came up with to
// represent a failure in this validation.
"underage",
// the third parameter is the default human-readble text that represents this
// type of failure when the errorcode lookup cannot be resolved.
"You are not old enough."
);
}
}
}
Error codes are discussed below and they are typically used to lookup the human-readable message text from a MessageSource.
Running the Validator
To run the validator you need an implemention of Errors. There are a few implementations and we’ll use BeanPropertyBindingResult because thats what Spring MVC would normally provide you.
Errors result = new BeanPropertyBindingResult(person, "person");
userValidator.validate(person, result);
if( result.hasErrors() ){
result.getAllErrors().stream().forEach(System.out::println);
}
After running a Validator the Errors bag can be inspected for results. You can even pass the Errors bag to multiple validators.
Errors result = new BeanPropertyBindingResult(person, "person");
personNameValidator.validate(person, result);
personAgeValidator.validate(person, result);
// etc
// now you have a collection of errors from all the validators
if( result.hasErrors() ){
result.getAllErrors().stream().forEach(System.out::println);
}
Think about separating validators as it may or may not be a good idea depending on what you’re trying to accomplish.
Validation with Annotations (JSR-303)
It is also possible to define validations as annotations directly on the properties of a Bean. This can be convenient for simple validations like null checks, lengths, or even more complicated custom validations.
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.*;
@Data
@NoArgsConstructor
public class Pet {
@NotNull
@Size(min = 2, max = 25)
private String name;
@NotNull
private String type;
@NotNull
@Max(30)
private Integer age;
@NotNull
private Integer weight;
}
Now you just need to apply the @Valid annotation on your payload in a Spring Controller to have Spring run the JSR-303 annotation validations.
@PostMapping("/pets")
public Pet addPet(@RequestBody @Valid Pet pet){
// code
}
We haven’t defined anyway to inspect the Errors so Spring will return a payload that looks like this by default if a validation fails.
{
"error": "Bad Request",
"errors": [
{
"arguments": [
{
"arguments": null,
"code": "type",
"codes": [
"pet.type",
"type"
],
"defaultMessage": "type"
}
],
"bindingFailure": false,
"code": "NotNull",
"codes": [
"NotNull.pet.type",
"NotNull.type",
"NotNull.java.lang.String",
"NotNull"
],
"defaultMessage": "must not be null",
"field": "type",
"objectName": "pet",
"rejectedValue": null
}
]
}
If you want to do something with the Errors then have Spring inject a BindingResult object for you (it extends Errors). Spring will handle the binding in the same way you did it before manually.
@PostMapping("/pets")
public Pet addPet(@RequestBody @Valid Pet pet, BindingResult result){
if( result.hasErrors() ){
result.getAllErrors().stream().forEach(System.out::println);
}
// code
}
Read: jcp.org: JSR 303: Bean Validation Read: wikipedia.org: Bean Validation Read: developer.ucsd.edu: Java Validation API (JSR-303)
Custom JSR-303 Annotation Validator
To make a custom JSR-303 validation you need two things - an annotation and a corresponding ConstraintValidator.
Here is an example of a custom annotation called @PetId that we’ll use to validate that a Pet ID takes the format of 5 digits followed by a dash then two letters (i.e., 12345-AZ).
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PetIdNumberValidator.class)
public @interface PetId {
String message() default "{com.example.PetId.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Here is the corresponding ConstraintValidator.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PetIdNumberValidator implements
ConstraintValidator<PetId, String> {
@Override
public void initialize(PetId petId) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.matches("[0-9]{5}-[A-Z]{2}")
&& (value.length() > 3) && (value.length() < 14);
}
}
Important: Spring will initialize ConstraintValidator’s as a Singletons so only one instance will ever be created for each type. Do not store state anywhere here.
Read: baeldung.com: Spring MVC Custom Validation Read: dolszewski.com: Custom validation annotation in Spring
ErrorCode, MessageSource and ResourceBundle
Externalization of error messages is just as important as application properties. The process by which you do this is slightly different, instead of properties you have messages and instead of calling them property files they are resource bundles. This is an important step to making your system maintainable and support localization. To accomplish this you need a file somewhere in your classpath that is formatted as a key/value pair and loaded into the system as a ResourceBundle.
The entire process of mapping codes to message text is called message interpolation.
Read: spring.io: Internationalization using MessageSource Read: baeldung.com: Spring Boot Internationalization Read: wikipedia.org: Java Resource Bundle Read: oracle.com: ResourceBundle Read: oracle.com: ResourceBundle Concept Read: jboss.org: Message Interpolation
Hibernate Validation.properties
Hibernate validations will by default source their defaultMessage from a file on the classpath provided by Hibernate Validator called Validation.properties.
messages.properties convention
There are several ways to load a bundle and Spring provides a very convenient property called spring.messages.basename where you can set the location of the files. Spring Boot is smart enough to assume convention of course so by default it will look for messages.properties and load that as a resource bundle for you. Here is an example of a simple resource bundle:
Bundles support Locale change with the format messages_XX.properties, where XX is the locale code.
src/main/resources/messages.properties
# Override default Spring Validation Field Error Messages
field.required=Required field
field.max_length=Field cannot be greater than {0} characters
field.min_length=Field cannot be less than {0} characters
underage=You are only {0}, you must be at least 21!
This is currently broken in Spring.
API Error Framework
If you’re building a more sophisticated API then you would want more control over how the error responses are generated. This means mapping every type of exception into your own ApiError facade.