Reflectoring - Io-Validation With Spring Boot - The Complete Guide
Reflectoring - Io-Validation With Spring Boot - The Complete Guide
reflectoring.io/bean-validation-with-spring-boot
August 5, 2021
Spring Boot
Bean Validation is the de-facto standard for implementing validation logic in the Java
ecosystem. It’s well integrated with Spring and Spring Boot.
However, there are some pitfalls. This tutorial goes over all major validation use cases and
sports code examples for each.
Example Code
This article is accompanied by a working code example on GitHub.
implementation('org.springframework.boot:spring-boot-starter-validation')
It’s not necessary to add the version number since the Spring Dependency Management
Gradle plugin does that for us. If you’re not using the plugin, you can find the most recent
version here.
However, if we have also included the web starter, the validation starter comes for free:
implementation('org.springframework.boot:spring-boot-starter-web')
Note that the validation starter does no more than adding a dependency to a compatible
version of hibernate validator, which is the most widely used implementation of the Bean
Validation specification.
1/15
@NotBlank : to say that a string field must not be the empty string (i.e. it must have
at least one character).
@Min and @Max : to say that a numerical field is only valid when it’s value is
above or below a certain value.
@Pattern : to say that a string field is only valid when it matches a certain regular
expression.
@Email : to say that a string field must be a valid email address.
class Customer {
@Email
private String email;
@NotBlank
private String name;
// ...
}
Validator
To validate if an object is valid, we pass it into a Validator which checks if the constraints
are satisfied:
In many cases, however, Spring does the validation for us. We don’t even need to create a
validator object ourselves. Instead, we can let Spring know that we want to have a certain
object validated. This works by using the the @Validated and @Valid annotations.
The @Validated annotation is a class-level annotation that we can use to tell Spring to
validate parameters that are passed into a method of the annotated class. We’ll learn more
about how to use it in the section about validating path variables and request parameters.
We can put the @Valid annotation on method parameters and fields to tell Spring that
we want a method parameter or field to be validated. We’ll learn all about this annotation
in the section about validating a request body.
2/15
Let’s say we have implemented a Spring REST controller and want to validate the input
that' passed in by a client. There are three things we can validate for any incoming HTTP
request:
class Input {
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
// ...
}
We have an int field that must have a value between 1 and 10, inclusively, as defined by
the @Min and @Max annotations. We also have a String field that must contain an IP
address, as defined by the regex in the @Pattern annotation (the regex actually still
allows invalid IP addresses with octets greater than 255, but we’re going to fix that later in
the tutorial, when we’re building a custom validator).
To validate the request body of an incoming HTTP request, we annotate the request body
with the @Valid annotation in a REST controller:
@RestController
class ValidateRequestBodyController {
@PostMapping("/validateBody")
ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
return ResponseEntity.ok("valid");
}
We simply have added the @Valid annotation to the Input parameter, which is also
annotated with @RequestBody
to mark that it should be read from the request body. By doing this,
we’re telling Spring to pass the object to a Validator before doing anything else.
3/15
Use @Valid on Complex Types
If the Input class contains a field with another complex type that should be validated,
this field, too, needs to be annotated with @Valid .
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateRequestBodyController.class)
class ValidateRequestBodyControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void whenInputIsInvalid_thenReturnsStatus400() throws Exception {
Input input = invalidInput();
String body = objectMapper.writeValueAsString(input);
mvc.perform(post("/validateBody")
.contentType("application/json")
.content(body))
.andExpect(status().isBadRequest());
}
}
You can find more details about testing Spring MVC controllers in my article about the
@WebMvcTest annotation.
We’re not validating complex Java objects in this case, since path variables and request
parameters are primitive types like int or their counterpart objects like Integer or
String .
Instead of annotating a class field like above, we’re adding a constraint annotation (in this
case @Min ) directly to the method parameter in the Spring controller:
4/15
@RestController
@Validated
class ValidateParametersController {
@GetMapping("/validatePathVariable/{id}")
ResponseEntity<String> validatePathVariable(
@PathVariable("id") @Min(5) int id) {
return ResponseEntity.ok("valid");
}
@GetMapping("/validateRequestParameter")
ResponseEntity<String> validateRequestParameter(
@RequestParam("param") @Min(5) int param) {
return ResponseEntity.ok("valid");
}
}
Note that we have to add Spring’s @Validated annotation to the controller at class level
to tell Spring to evaluate the constraint annotations on method parameters.
The @Validated annotation is only evaluated on class level in this case, even though it’s
allowed to be used on methods (we’ll learn why it’s allowed on method level when
discussing validation groups later).
If we want to return a HTTP status 400 instead (which makes sense, since the client
provided an invalid parameter, making it a bad request), we can add a custom exception
handler to our contoller:
@RestController
@Validated
class ValidateParametersController {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<String>
handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("not valid due to validation error: " +
e.getMessage(), HttpStatus.BAD_REQUEST);
}
Later in this tutorial we will look at how to return a structured error response that
contains details on all failed validations for the client to inspect.
5/15
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateParametersController.class)
class ValidateParametersControllerTest {
@Autowired
private MockMvc mvc;
@Test
void whenPathVariableIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validatePathVariable/3"))
.andExpect(status().isBadRequest());
}
@Test
void whenRequestParameterIsInvalid_thenReturnsStatus400() throws Exception {
mvc.perform(get("/validateRequestParameter")
.param("param", "3"))
.andExpect(status().isBadRequest());
}
@Service
@Validated
class ValidatingService{
Again, the @Validated annotation is only evaluated on class level, so don’t put it on a
method in this use case.
6/15
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceTest {
@Autowired
private ValidatingService service;
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
We usually don't want to do validation as late as in the persistence layer because it means
that the business code above has worked with potentially invalid objects which may lead
to unforeseen errors. More on this topic in my article about Bean Validation anti-patterns.
Let’s say want to store objects of our Input class to the database. First, we add the
necessary JPA annotation @Entity and add an ID field:
@Entity
public class Input {
@Id
@GeneratedValue
private Long id;
@Min(1)
@Max(10)
private int numberBetweenOneAndTen;
@Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
private String ipAddress;
// ...
Then, we create a Spring Data repository that provides us with methods to persist and
query for Input objects:
7/15
By default, any time we use the repository to store an Input object whose constraint
annotations are violated, we’ll get a ConstraintViolationException as this
integration test demonstrates:
@ExtendWith(SpringExtension.class)
@DataJpaTest
class ValidatingRepositoryTest {
@Autowired
private ValidatingRepository repository;
@Autowired
private EntityManager entityManager;
@Test
void whenInputIsInvalid_thenThrowsException() {
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
repository.save(input);
entityManager.flush();
});
}
You can find more details about testing Spring Data repositories in my article about the
@DataJpaTest annotation.
Note that Bean Validation is only triggered by Hibernate once the EntityManager is
flushed. Hibernate flushes thes EntityManager automatically under certain
circumstances, but in the case of our integration test we have to do this by hand.
If for any reason we want to disable Bean Validation in our Spring Data repositories, we
can set the Spring Boot property
spring.jpa.properties.javax.persistence.validation.mode to none .
In the Input class from above, we used a regular expression to validate that a String is a
valid IP address. However, the regular expression is not complete: it allows octets with
values greater than 255 (i.e. “111.111.111.333” would be considered valid).
Let’s fix this by implementing a validator that implements this check in Java instead of
with a regular expression (yes, I know that we could just use a more complex regular
expression to achieve the same result, but we like to implement validations in Java, don’t
we?).
8/15
@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern =
Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]
{1,3})$");
Matcher matcher = pattern.matcher(value);
try {
if (!matcher.matches()) {
return false;
} else {
for (int i = 1; i <= 4; i++) {
int octet = Integer.valueOf(matcher.group(i));
if (octet > 255) {
return false;
}
}
return true;
}
} catch (Exception e) {
return false;
}
}
}
9/15
We can now use the @IpAddress annotation just like any other constraint annotation:
class InputWithCustomValidator {
@IpAddress
private String ipAddress;
// ...
Validating Programmatically
There may be cases when we want to invoke validation programmatically instead of
relying on Spring’s built-in Bean Validation support. In this case, we can use the Bean
Validation API directly.
class ProgrammaticallyValidatingService {
@Service
class ProgrammaticallyValidatingService {
ProgrammaticallyValidatingService(Validator validator) {
this.validator = validator;
}
10/15
When this service is instantiated by Spring, it will automatically have a Validator
instance injected into the constructor.
The following unit test proves that both methods above work as expected:
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProgrammaticallyValidatingServiceTest {
@Autowired
private ProgrammaticallyValidatingService service;
@Test
void whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInput(input);
});
}
@Test
void givenInjectedValidator_whenInputIsInvalid_thenThrowsException(){
Input input = invalidInput();
assertThrows(ConstraintViolationException.class, () -> {
service.validateInputWithInjectedValidator(input);
});
}
Let’s take the typical CRUD operations, for example: the “Create” use case and the
“Update” use case will most probably both take the same object type as input. However,
there may be validations that should be triggered under different circumstances:
The Bean Validation feature that allows us to implement validation rules like
this is called “Validation Groups”.
We have already seen that all constraint annotations must have a groups field. This can
be used to pass any classes that each define a certain validation group that should be
triggered.
11/15
For our CRUD example, we simply define two marker interfaces OnCreate and
OnUpdate :
interface OnCreate {}
interface OnUpdate {}
We can then use these marker interfaces with any constraint annotation like this:
class InputWithGroups {
@Null(groups = OnCreate.class)
@NotNull(groups = OnUpdate.class)
private Long id;
// ...
This will make sure that the ID is empty in our “Create” use case and that it’s not empty in
our “Update” use case.
@Service
@Validated
class ValidatingServiceWithGroups {
@Validated(OnCreate.class)
void validateForCreate(@Valid InputWithGroups input){
// do something
}
@Validated(OnUpdate.class)
void validateForUpdate(@Valid InputWithGroups input){
// do something
}
Note that the @Validated annotation must again be applied to the whole class. To
define which validation group should be active, it must also be applied at method level.
To make certain that the above works as expected, we can implement a unit test:
12/15
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceWithGroupsTest {
@Autowired
private ValidatingServiceWithGroups service;
@Test
void whenInputIsInvalidForCreate_thenThrowsException() {
InputWithGroups input = validInput();
input.setId(42L);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForCreate(input);
});
}
@Test
void whenInputIsInvalidForUpdate_thenThrowsException() {
InputWithGroups input = validInput();
input.setId(null);
assertThrows(ConstraintViolationException.class, () -> {
service.validateForUpdate(input);
});
}
Using validation groups can easily become an anti-pattern since we're mixing concerns.
With validation groups the validated entity has to know the validation rules for all the use
cases (groups) it is used in. More on this topic in my article about Bean Validation anti-
patterns.
First, we need to define that data structure. We’ll call it ValidationErrorResponse and
it contains a list of Violation objects:
13/15
public class ValidationErrorResponse {
// ...
}
// ...
}
@ControllerAdvice
class ErrorHandlingControllerAdvice {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onConstraintValidationException(
ConstraintViolationException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
error.getViolations().add(
new Violation(violation.getPropertyPath().toString(),
violation.getMessage()));
}
return error;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
error.getViolations().add(
new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
}
return error;
}
What we’re doing here is simply reading information about the violations out of the
exceptions and translating them into our ValidationErrorResponse data structure.
14/15
Note the @ControllerAdvice annotation which makes the exception handler methods
available globally to all controllers within the application context.
Conclusion
In this tutorial, we’ve gone through all major validation features we might need when
building an application with Spring Boot.
If you want to get your hands dirty on the example code, have a look at the github
repository.
Update History
2021-08-05: updated and polished the article a bit.
2018-10-25: added a word of caution on using bean validation in the persistence
layer (see this thread on Twitter).
15/15