Spring Boot validators fail to return custom error messages when the
@Valid annotation is used without proper binding or when the validation logic in the controller does not correctly process the
BindingResult object. This issue typically stems from missing imports, incorrect method signatures, or a misunderstanding of how JSR-380 constraints interact with Spring MVC's data binding mechanism. By ensuring that your controller methods accept a
BindingResult parameter immediately after the validated object and by verifying that your entity classes are properly annotated, you can resolve these silent failures and ensure that user-friendly, custom error messages are returned in the API response.
Understanding the Validation Flow
To debug why custom messages are not appearing, it is essential to understand the lifecycle of request validation in Spring Boot. When a client sends a POST or PUT request to an endpoint protected by validation, Spring MVC uses a
Validator implementation (usually
LocalValidatorFactoryBean) to check the incoming data against the constraints defined on the model class. If violations occur, they are stored in a
BindingResult object associated with the specific field or object being validated.
The most common reason for missing custom messages is that the
BindingResult is not being checked in the controller. If you annotate your controller method with
@Valid but do not include a
BindingResult argument immediately following the model parameter, Spring will throw a
MethodArgumentNotValidException. While this exception contains the validation errors, if your global exception handler is not configured to extract the specific message from the constraint violation, the default system message may be returned instead of your custom one.
Another frequent oversight involves the placement of the
@Valid annotation. It must be placed on the parameter representing the DTO or Entity class. Additionally, ensure that you have imported
javax.validation.Valid (or
jakarta.validation.Valid in Spring Boot 3.x) rather than a different package that might not trigger the automatic validation process. Without this correct import, Spring treats the object as a regular bean and skips the validation phase entirely, meaning no error messages are generated at all.
Can't Access Static Resources in Spring Boot Application
Configuring Custom Messages in Annotations
Custom error messages are defined directly within the constraint annotations on your model fields. For example, if you want to customize the message for a
@NotBlank field, you modify the annotation as follows:
@NotBlank(message = "Username cannot be empty")
private String username;
If you are comparing two fields, such as checking if a password matches a confirmation password, you need a custom validator class. Create a class that implements
ConstraintValidator<YourAnnotation, YourObject>. In the
isValid method, perform your logic. To associate a message with this custom annotation, define it in the annotation itself:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordMatch {
String message() default "Passwords do not match";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
Ensure that the
message attribute in the annotation definition matches the string you expect to see. If you rely on externalized messages in
messages.properties, ensure that the key corresponds exactly to the
message value or use the
message attribute to reference the property file key using the
${...} syntax.
Handling Validation Errors in the Controller
Once validation runs, the controller must explicitly handle the results. The standard pattern is to check if the
BindingResult has any errors before proceeding with business logic.
> register(@Valid @RequestBody UserRegistrationDto registration, BindingResult result) {
Map errors = new HashMap<>();
if (result.hasErrors()) {
result.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
// Proceed with registration
return ResponseEntity.ok().build();
}
In this snippet, we iterate over
FieldErrors to build a map of field names to their custom messages. If you are using a global exception handler, you might prefer to let the exception propagate and catch it in a
@ControllerAdvice class. However, for immediate feedback, checking
BindingResult directly gives you full control over the response structure. Ensure that
error.getDefaultMessage() returns your custom text; if it returns null, check that the annotation was correctly processed and that no default fallback is overriding your setting.
Troubleshooting OAuth2.0 Access Token Response
Debugging Common Pitfalls
Several subtle issues can prevent custom messages from displaying. First, verify that your project includes the necessary dependencies. For Spring Boot 2.x, you need
spring-boot-starter-validation which brings in Hibernate Validator. For Spring Boot 3.x, this is included by default, but ensure you are using
jakarta.validation packages.
Second, check for conflicting validators. If you have multiple validators running on the same field, ensure they are not overriding each other’s messages. Third, inspect the HTTP response headers and body using tools like Postman or curl. Sometimes the message is present in the JSON response, but the frontend client fails to parse it correctly, leading developers to believe the backend is failing.
Finally, ensure that your DTOs are immutable or have proper constructors. If Spring cannot instantiate the object to bind data, validation may fail silently or throw a different exception. Using Lombok’s
@Data or
@Builder can help, but ensure that the builder pattern is compatible with Jackson deserialization if you are using
@RequestBody.
Frequently Asked Questions
Why is my custom message showing the default constraint message?
This usually happens because the
message attribute in your annotation was not set correctly, or you are referencing a property key that does not exist in your
messages.properties file. Check that the string literal in the
message attribute matches your intended output, or verify that the property file is loaded in the classpath.
How do I validate two fields together, like password and confirm password?
You need to create a custom annotation at the class level, not the field level. Implement a
ConstraintValidator that takes the entire object as input, allowing you to compare both fields. Apply this annotation to the DTO class and ensure the controller validates the object, triggering the custom validator logic.
Can I use internationalization (i18n) for validation messages?
Yes, Spring supports i18n for validation messages. You can define keys in a
ValidationMessages.properties file and reference them in your annotations using the
${key} syntax. Ensure that the
LocalValidatorFactoryBean is configured to use a
MessageSource that resolves these properties based on the current locale.
What if I get a MethodArgumentNotValidException instead of my custom errors?
This exception indicates that validation failed, but the error was not handled gracefully in the controller. You should either add a
BindingResult parameter to your controller method to handle errors manually, or implement a
@ControllerAdvice with a
@ExceptionHandler(MethodArgumentNotValidException.class) to convert the exception into a structured response containing your custom messages.
Does @Valid work on nested objects automatically?
Yes, if you have a nested object inside your DTO, you must place the
@Valid annotation on the field of the nested object. This tells Spring to recursively validate the nested object. Without
@Valid on the nested field, only the top-level fields will be validated, and any custom messages in the nested class will be ignored.