Marc Denning

Validating Requests with Spring Boot 3

A few years ago, I documented my experience setting up Spring Boot to perform robust validation of HTTP requests. I've just gotten around to upgrading that repository to the Spring Boot 3 release, and I want to update my previous findings and share with you my experience upgrading the sample repo.

This post will walk you through what it was like to upgrade from Spring Boot version 2 to version 3 and all of the changes necessary to be compatible both with the latest Java LTS - version 21 - and Spring Boot 3.

Getting Started

To start, I walked through typical steps for upgrading your Spring Boot app:

  1. Upgrade Gradle to the latest release (8.5) as well as the project SDK to Java 21 using the built-in wrapper command. Upgrading Gradle and the Java SDK also meant updating the IntelliJ project and run configurations.
  2. Update the Gradle plugins in build.gradle to pull in the latest versions of Spring Boot.
  3. Sync the Gradle build settings with ./gradlew tasks. This pulls in new plugin versions and validates the Gradle configuration. JUnit 5 is now the default, so the useJUnitPlatform() method call can be removed. Because of dependency changes, Hibernate Validator now needs to be included explicitly.
  4. Finally, execute an application build to see what warnings and errors turn up: ./gradlew clean build.

Update Namespaces

The very first error that jumped out is that usage of annotations and interfaces referencing the javax namespace did not resolve. The jakarta namespace has replaced javax for many of the interfaces, annotations, and classes that fulfilled JEE specifications, including validation constraints. In the validation sample project, the only change I made was updating the namespace from javax to jakarta.

Refactor Spring Bindings

Getting deeper into the weeds of validation error handling, I learned that Spring has changed some of its default error handlers for RestController classes. The Spring team implemented support for RFC 7807 which specifies some standards for HTTP error responses. If you have used JSON API or OData before, you may be familiar with having a standard error format, and it's something I always encourage. This RFC is new to me, so I'm still processing it, but I welcome the idea that there could be an open standard that API specifications reference, implement, and extend. It affords an opportunity to make error handling across system integrations more consistent and robust which in turn helps developers, users, and business building those integrations.

In order to take advantage of the new work the Spring team has done, I stopped extending the ResponseEntityExceptionHandler class, consolidated handler methods, and removed my custom Error POJO. In testing, it seems that even without custom exception handling, Spring Boot 3 does a better job reporting an appropriate status code and usable HTTP response because of the RFC implementation and new ErrorResponse class. The ApiExceptionHandler now looks like:

@RestControllerAdvice
public class ApiExceptionHandler {

    private static final Logger logger = Logger.getLogger(ApiExceptionHandler.class.getName());

    private final MessageSource messageSource;

    public ApiExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    public ErrorResponse handleMethodArgumentNotValid(
            BindException ex,
            Locale locale
    ) {
        final ObjectError error = ex.getBindingResult().getAllErrors().getFirst();
        final String message = messageSource.getMessage(error, locale);

        logger.log(Level.SEVERE, ex.getMessage(), ex);

        return ErrorResponse.create(ex, HttpStatus.BAD_REQUEST, message);
    }
}

The one handler implemented handles both the MethodArgumentNotValidException and more generic BindException. Take a look at the PersonController to see where each is used:

@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);
    }
}

The MethodArgumentNotValidException is triggered automatically by Spring when processing the @Valid annotation on the @RequestBody parameter. BindException is thrown when the custom Validator for the Person class returns errors.

Migrate Error Messages

Once I had adjusted the RestControllerAdvice, I tested the API endpoint again. Now, I was seeing that the custom error messages were not both showing up. Doing some more research, I found that validation messages can now just be included in the messages.properties resource bundle which is be picked up by both Hibernate Validator and Spring Validator.

There are multiple property names that can be used for custom constraints and validators, so please read Spring's documentation on message code resolution for more info.

With the custom messages, arguments may be provided to serialize in messages using numeric placeholders. For instance, the error message corresponding to MyValidator is coded as Name must end with {0}.. In the MyValidator class, the error code is recorded with the field error along with arguments that can be interpolated at runtime. This helps provide even more context to users and developerse debugging an issue.

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

    private final 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", "MyValidator", new Object[] {suffix}, "Incorrect suffix.");
        }
    }
}

Closing Thoughts and Notes on the Repository

Overall, upgrading from Spring Boot version 2 to version 3 was not a difficult upgrade. That said, the project I started from was very simple and bare bones compared to a typical production API.

I really appreciate the implementation of RFC 7807 as a useful default error handler, and I will definitely be exploring that more. Its implementation in Spring does afford API developers an opportunity to simplify a bit of their error handling logic and hopefully clean up some code. It took me a bit to migrate to the new model, but I was ultimately able to reduce the lines of code in this repository.

If you want to review the whole sample, please check out the spring-api-validation-sample repository on GitHub. I've merged the Spring Boot 3 upgrade into the main branch, and left a release/spring-boot-2 branch out there for Spring Boot 2 developers. If you see any issues or opportunities for improvement, please open an issue or pull request! I welcome the discussion and collaboration.

Otherwise, I hope this helps you in your upgrade of validation and error handling in your Spring Boot applications!