Skip to content
Spring Boot sb validation 3 min read

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:

SourceExceptionCarries
@Valid @RequestBodyMethodArgumentNotValidExceptiona BindingResult of FieldErrors
@Validated params / propertiesConstraintViolationExceptiona 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: ConstraintViolationException is not handled by ResponseEntityExceptionHandler, so always add an explicit @ExceptionHandler for it — otherwise param-validation failures fall through to a 500.

Tips for great validation responses

  • Use a LinkedHashMap so 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 @RestControllerAdvice can map your domain exceptions to ProblemDetail too. See Controller Advice and ProblemDetail.

Last updated June 13, 2026
Was this helpful?