Marc Denning

Validating Requests in Spring Boot

A frequent feature I find myself implementing for APIs is some kind of domain model validation. This is either because there are business requirements related to the data being handled or because of a need to protect the system.

In a Spring Boot application, a lot of validation conveniences are afforded to developers, but implementing good validation practices still requires some effort that I would call non-trivial (as in, I cannot remember these tools off-hand). In this post, I document some reminders and tips for implementing validation in Spring since I went through this exercise recently.

If you are looking for a quick code reference, I have a complete Spring Boot app published on GitHub that accompanies this post. Otherwise, to start off, assume that our project already has the following characteristics:

From here, we will layer on different validation practices including:

At the beginning of each section is a list of project and framework classes that are relevant.

Basic Validation Annotations

Classes in play:

I like annotating my domain models with validation attributes as much as possible to define what makes the model valid. I find that it keeps the validation requirements close to the model making it easier to maintain and inspect. We'll start here, but of course there are cases when standard annotations are not enough.

Common validation annotations are in the javax.validation.constraints package. These are annotations such as @NotNull, @NotEmpty, and @Size. These annotations are a great place to start documenting your model's validation requirements. We simply add these annotations to the fields of our model. For example:

public class Person {

    @NotBlank
    private String name;

    //...
}

Spring Boot brings in the Hibernate Validator project which is a common implementation of the validation interfaces in the javax.validation package. This means that with a Spring Boot project, simply annotating our domain objects with validation constraints is all we need to get started!

In our controller class, we apply the @Valid annotation to the domain argument that is, for example, also annotated with @RequestBody:

@RestController
@RequestMapping(path = {"/api/people"})
public class PersonController {

    @PostMapping
    public ResponseEntity<Person> createPerson(@Valid @RequestBody Person person) {
        return ResponseEntity.ok(person);
    }
}

This annotation tells Spring to invoke the configured validator on the model. If the object provided does not pass all of the configured constraints, Spring updates the DataBinder object that can be injected into our @RequestMapping methods and it throws an exception.

If you're writing an app as you go along, try spinning up Spring at this point and submitting an invalid request body such as:

{
  "name": "" 
}

Observe the HTTP status code and the body that comes back from this request. With just a few annotations, the API is already providing some protections against invalid data and some clues to API consumers about the problem.

Custom Validation Constraints

Classes in play:

The annotations are great, but as I mentioned, sometimes our validation requirements are a bit more specific or detailed than the out-of-the-box annotations allow.

I won't go into depth about all of Spring's validation features because the framework authors do a better job in the Spring Framework documentation on validation. However, an important detail I want to highlight is that while there are built in validators, we can also configure our own implementations and register them with Spring and the Hibernate Validator so that they get invoked automatically.

To declare our own validation constraint, we start by declaring a @Constraint annotation:

@Target({TYPE, METHOD, FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {MyConstraintValidator.class})
public @interface MyConstraint {

    /**
     * Provide validation failed message. Default resolves to configurable property.
     *
     * @return validation failed message
     */
    String message() default "{MyConstraint}";

    /**
     * Configure constraint groups that can be selected together.
     *
     * @return configured groups
     */
    Class<?>[] groups() default { };

    /**
     * Allow extensibility of the constraint.
     *
     * @return payload object
     */
    Class<? extends Payload>[] payload() default { };
}

Notice that we also link this annotation to a validator class:

public class MyConstraintValidator
    implements ConstraintValidator<MyConstraint, String> {

    @Override
    public void initialize(MyConstraint constraintAnnotation) {
        // Perform any initialization of the instance of the constraint annotation
        // For example, assert that extra parameters are valid
    }

    @Override
    public boolean isValid(String fieldValue, ConstraintValidatorContext context) {
        return fieldValue == null || fieldValue.startsWith("Brilliant");
    }
}

This example validator is certainly trivialized, but it demonstrates the point. Notice also that the class has no dependencies. This means that it can be instantiated by the Hibernate Validator via an empty constructor. It is possible to implement more stateful validators, but this is by far the easiest, so I recommend keeping your validations stateless as much as possible.

Now, this ConstraintValidator can be invoked if we add our @MyConstraint annotation to our domain model:

public class Person {

    @NotBlank
    @MyConstraint
    private String name;

    //...
}

Now, Spring has enough information to validate our domain object, including our custom validation constraint. If you're following along, spin up the application and try to send in an invalid object:

{
  "name": "Silly Marc"
}

Customize Error Messages

Classes in play:

We can implement precise and complex model validations, but the default error handling and messaging for these constraints may not meet our project requirements or communicate helpful information to developers working with our API.

One way to customize the error handling for validation errors like these is to implement an @ExceptionHandler method for a MethodArgumentNotValidException. This is commonly declared on a @ControllerAdvice class.

First, our domain model for an error:

public class Error {

    private String message;

    public Error(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Now, the exception handler:

@ControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Error> handleMethodArgumentNotValidException(
          MethodArgumentNotValidException exception, WebRequest request
    ) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(new Error(exception.getMessage()));
    }
}

Obviously, we're keeping this simple for now. The data structure provided back from this method could be any domain object needed to meet our project's requirements. Also note that other classes can be injected into this method as needed like WebRequest which goes unused here.

In some cases, we may even want to override Spring's default exception handling. For this, I found success in having the ApiExceptionHandler we implemented above extend Spring's ResponseEntityExceptionHandler. From there, we can override different methods of that class to customize common errors. In particular, the handleExceptionInternal method is designed to be overridden for a common exit point to massage errors. Here, we'll just adapt our method above to override the handleMethodArgumentNotValid method that Spring provides:

//...
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @Override
    public ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request
    ) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .headers(headers)
            .body(new Error(exception.getMessage()));
    }
//...

Once again, try submitting an invalid request and observe how the response body looks different than it does with Spring's build in error structure.

Implementing a Stateful Validator

Classes in play:

What if we need a place for more stateful validation logic? For this, we can implement Spring's Validator interface and register the bean with Hibernate's Validator:

@Component("myValidator")
public class MyValidator implements Validator {

    private String suffix;

    public MyValidator(@Value("my.validation.person.suffix") String suffix) {
        this.suffix = suffix;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Assert.isInstanceOf(
                Person.class,
                target,
                "Argument to MyValidator must be of type Person. Object is of type "
        );
        final Person providedPerson = (Person) target;

        if (providedPerson.getName() != null && !providedPerson.getName().endsWith(suffix)) {
            errors.rejectValue("name", String.format("Name must end with %s.", suffix));
        }
    }
}

@Component
public class SpringValidatorHibernatePropertiesCustomizer implements HibernatePropertiesCustomizer {

    private final Validator validator;

    public SpringValidatorHibernatePropertiesCustomizer(Validator validator) {
        this.validator = validator;
    }

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put("javax.persistence.validation.factory", validator);
    }
}

The bean is named explicitly for convenience because there are likely multiple implementations of Validator in Spring's context, and we want to be sure to be able to distinguish which one we are invoking. Also notice that in our validator, we inject a property from Spring's context to simulate loading a dependency. This could be any Spring-managed @Bean or @Component.

As of right now, we have to tell the Hibernate Validator about Spring's built-in Validator in order for it to pick up our new MyValidator class when validating domain objects. This may change in a future release of Spring.

We can also invoke our validator manually. Update our controller to inject our validator, and use it in our createPerson method:

@RestController
@RequestMapping(path = {"/api/people"})
public class PersonController {

    private final Validator validator;

    public PersonController(@Qualifier("myValidator") Validator validator) {
        this.validator = validator;
    }

    @PostMapping
    public ResponseEntity<Person> createPerson(
            @Valid @RequestBody Person person,
            BindingResult bindingResult
    ) throws BindException {
        validator.validate(person, bindingResult);

        if (bindingResult.hasErrors()) {
            throw new BindException(bindingResult);
        }

        return ResponseEntity.ok(person);
    }
}

In our exception handling code (ApiExceptionHandler above), we can add (or override) a handler for BindExceptions to overlay custom error rendering:

//...

    @Override
    protected ResponseEntity<Object> handleBindException(
            BindException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request
    ) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(new Error(ex.getMessage()));
    }

//...

Localizing Validation Messages

Classes in play:

The last detail of domain object validation I want to cover is localization. As an English-speaker working with mostly exclusively English speaking clients, it is tempting to just write an English error message into applications. However, Spring provides good faculties for localizing strings, and it is always easier to execute this in the early stages of a project when there are not messages littered all over the code base already.

The main interface Spring affords us for working with localized message is MessageSource. In any part of our application, we can inject MessageSource and invoke it:

messageSource.getMessage("ERROR_CODE", Locale.getDefault());

Note: this example pulls the locale from the system where the application is running via the call to Locale.getDefault(). This could and probably should be different if your API needs to respond with different languages. For example, the locale may be interpreted from the user agent header of an HTTP request or pulled from persistent storage of user settings.

For validation messages, Spring (and Hibernate Validator) will automatically look for a ValidationMessages resource bundle. This means that it is looking for properties files in the classpath prefixed with ValidationMessages. Default messages already exist for constraints from the javax.validation.constraints package, but can be overridden here by providing a key with the name of the constraint (ex. NotNull).

We can also add a key for the custom constraint we wrote earlier with the same value we declared for the message property in the annotation itself. We need to create a file ValidationMessages.properties in our src/resources directory to help Spring pick up the resources bundle. That file can remain empty, but we need to add files for the languages we need to support. For US English, this would be ValidationMessages_en_US.properties:

MyConstraint=The provided name must start with "Brilliant".

If we need to pull the error message out manually (ex. in an @ExceptionHandler method), we can use the same MessageSource interface combined with Spring's BindingResult:

final ObjectError error = bindingResult.getAllErrors().get(0);
final String message = messageSource.getMessage(error, Locale.getDefault());

Wrapping Up

With these examples, I hope that API validation is just a little bit easier in your Spring Boot projects going forward.

To recap, we covered:

I imagine that there is nuance that I missed and scenarios that I did not cover, so if you have any feedback, suggestions, or questions, feel free to reach out on Twitter.

Finally, in case you missed it before, you can find a full project sample on GitHub: spring-api-validation-sample.