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:
- 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. - Update the Gradle plugins in
build.gradle
to pull in the latest versions of Spring Boot. - 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 theuseJUnitPlatform()
method call can be removed. Because of dependency changes, Hibernate Validator now needs to be included explicitly. - 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!