Handling Validation Errors
Validation that fails with a vague 400 Bad Request is frustrating for API clients — they can’t tell what was wrong. A good API returns a structured, field-level error response. This page shows how to catch both validation exceptions in a @RestControllerAdvice and shape them into clean JSON, including the standards-based ProblemDetail format.
The two validation exceptions
Recall from earlier pages that Spring throws two different exceptions depending on where validation failed:
| Source | Exception | Carries |
|---|---|---|
@Valid @RequestBody | MethodArgumentNotValidException | a BindingResult of FieldErrors |
@Validated params / properties | ConstraintViolationException | a Set<ConstraintViolation> |
A complete handler covers both. We centralize them in a single @RestControllerAdvice so every controller benefits automatically.
A clean field-error response
First, define a small response shape. A record keeps it tidy:
public record ValidationErrorResponse(
int status,
String message,
Map<String, String> errors) {}
Now the advice, handling both exception types:
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestControllerAdvice
public class ValidationExceptionHandler {
// request-body validation
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse onBodyInvalid(MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));
return new ValidationErrorResponse(400, "Validation failed", errors);
}
// path / query param validation
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse onParamInvalid(ConstraintViolationException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getConstraintViolations().forEach(v -> {
String path = v.getPropertyPath().toString();
String field = path.substring(path.lastIndexOf('.') + 1); // trim method prefix
errors.put(field, v.getMessage());
});
return new ValidationErrorResponse(400, "Validation failed", errors);
}
}
Request:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"nope","age":12}'
Output:
{
"status": 400,
"message": "Validation failed",
"errors": {
"name": "must not be blank",
"email": "must be a well-formed email address",
"age": "must be greater than or equal to 18"
}
}
That is immediately actionable — the client sees exactly which fields failed and why.
Using ProblemDetail (RFC 9457)
Spring Boot 3 supports ProblemDetail, the standard “Problem Details for HTTP APIs” media type (application/problem+json). Returning it makes your errors interoperable with any client that understands the standard. See ProblemDetail responses for the full picture.
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail onBodyInvalid(MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "One or more fields are invalid");
problem.setTitle("Validation Failed");
problem.setProperty("errors", errors);
return problem;
}
Output (Content-Type: application/problem+json):
{
"type": "about:blank",
"title": "Validation Failed",
"status": 400,
"detail": "One or more fields are invalid",
"errors": {
"name": "must not be blank",
"email": "must be a well-formed email address",
"age": "must be greater than or equal to 18"
}
}
The type, title, status, and detail members are part of the spec; errors is a custom extension carried via setProperty.
Extending Spring’s built-in handler
Rather than handling MethodArgumentNotValidException from scratch, you can extend ResponseEntityExceptionHandler, which already converts framework exceptions into ProblemDetail. Override the validation hook to enrich it:
import org.springframework.http.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
ProblemDetail body = ex.getBody(); // pre-built ProblemDetail
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
body.setProperty("errors", errors);
return handleExceptionInternal(ex, body, headers, status, request);
}
}
This keeps Spring’s sensible defaults while adding the field-level errors map.
Note:
ConstraintViolationExceptionis not handled byResponseEntityExceptionHandler, so always add an explicit@ExceptionHandlerfor it — otherwise param-validation failures fall through to a500.
Tips for great validation responses
- Use a
LinkedHashMapso field order in the response is stable and predictable. - Return all field errors at once (the default) rather than one at a time — fewer round trips for the client.
- Don’t leak internals: the message should describe the input problem, not a stack trace.
- Keep the error shape consistent across every endpoint by centralizing it in one advice.
Tip: Make the validation advice part of a broader API error strategy — the same
@RestControllerAdvicecan map your domain exceptions toProblemDetailtoo. See Controller Advice and ProblemDetail.