skip to content

How do you produce a clean JSON error response from MethodArgumentNotValidException for a REST API?

level: seniorimportance: must knowfreq 70%

answer

  1. @RestControllerAdvice + @ExceptionHandler(MethodArgumentNotValidException)
  2. ex.getBindingResult().getFieldErrors()
  3. getField / getDefaultMessage / getRejectedValue
  4. Spring 6.1 ProblemDetail via ResponseEntityExceptionHandler
  5. also catch ConstraintViolationException + HttpMessageNotReadableException

basics

~10 s

Add a @RestControllerAdvice with an @ExceptionHandler(MethodArgumentNotValidException.class). Read ex.getBindingResult().getFieldErrors(), map each to field + message, and return a 400 with that structured body.

solid answer

~40 s

For REST, don't use BindingResult in the controller; let `MethodArgumentNotValidException` propagate and handle it centrally in a `@RestControllerAdvice`. In the handler, call `ex.getBindingResult().getFieldErrors()` to get field name (`getField()`), message (`getDefaultMessage()`), and rejected value; collect global/class-level errors via `getGlobalErrors()`. Build a stable error envelope (e.g., a list of `{field, message}`) and return `400`. Since Spring 6.1 you can alternatively extend `ResponseEntityExceptionHandler` and override `handleMethodArgumentNotValid`, which integrates with the ProblemDetail (RFC 7807) support. Watch for edge cases: internationalize messages via the `MessageSource`/`messageSource` codes on each `FieldError`; deduplicate multiple violations on one field; and handle `HttpMessageNotReadableException` and `ConstraintViolationException` (from method-level `@Validated`) in the same advice so the client sees a consistent contract.

code

java · 27 lines
java
@RestControllerAdvice
class ApiExceptionHandler {

    record FieldMessage(String field, String message) {}
    record ApiError(List<FieldMessage> errors) {}

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    ApiError onBodyInvalid(MethodArgumentNotValidException ex) {
        List<FieldMessage> errors = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(fe -> new FieldMessage(fe.getField(), fe.getDefaultMessage()))
            .toList();
        return new ApiError(errors);
    }

    // Method-level @Validated on @RequestParam/@PathVariable
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    ApiError onParamInvalid(ConstraintViolationException ex) {
        List<FieldMessage> errors = ex.getConstraintViolations().stream()
            .map(v -> new FieldMessage(v.getPropertyPath().toString(),
                                       v.getMessage()))
            .toList();
        return new ApiError(errors);
    }
}

go deeper

for a junior

Know that a @ControllerAdvice/@ExceptionHandler can turn the exception into custom JSON.

for a middle

Extract field errors from getBindingResult() and set status 400.

for a senior

Cover ConstraintViolationException, HttpMessageNotReadableException, and ProblemDetail integration for a uniform contract.

for a principal

Own the org-wide error envelope: i18n via MessageSource, secret redaction, RFC 7807 adoption, and versioned stability.

## The goal A raw `MethodArgumentNotValidException` produces Spring's default error JSON, which leaks internals and isn't a stable contract. You want a predictable body like: ```json { "errors": [ { "field": "email", "message": "must be a valid email" } ] } ``` ## Central handling with @RestControllerAdvice `@RestControllerAdvice` = `@ControllerAdvice` + `@ResponseBody`; its `@ExceptionHandler` methods apply across all controllers and serialize return values as JSON. ```java @RestControllerAdvice class ValidationExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) ApiError onInvalid(MethodArgumentNotValidException ex) { var fieldErrors = ex.getBindingResult().getFieldErrors().stream() .map(fe -> new FieldMessage(fe.getField(), fe.getDefaultMessage())) .toList(); return new ApiError(fieldErrors); } } ``` ## What the BindingResult gives you - `getFieldErrors()` → `List<FieldError>`: `getField()`, `getDefaultMessage()`, `getRejectedValue()`, `getCode()`, `getCodes()` (message-resolution codes for i18n). - `getGlobalErrors()` → `List<ObjectError>`: class-level constraints (e.g., a custom cross-field `@FieldsMatch` on the type). - `getAllErrors()` → both combined. ## Spring 6.1+ ProblemDetail path Modern Spring supports **RFC 7807 `ProblemDetail`**. Extend `ResponseEntityExceptionHandler` and override: ```java @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders h, HttpStatusCode status, WebRequest req) { ProblemDetail pd = ex.getBody(); // pre-populated ProblemDetail pd.setProperty("errors", ...); return handleExceptionInternal(ex, pd, h, status, req); } ``` This yields `application/problem+json` responses consistently. ## Edge cases and gotchas 1. **Method-level validation is a different exception.** Constraints on `@RequestParam`/`@PathVariable` (with `@Validated` on the class) throw `jakarta.validation.ConstraintViolationException` (via `HandlerMethodValidationException` in newer Spring), **not** `MethodArgumentNotValidException`. Handle both. 2. **Malformed JSON** → `HttpMessageNotReadableException`; handle for a consistent 400. 3. **Message i18n**: `getDefaultMessage()` is already resolved by the validator, but you can re-resolve via `MessageSource` using `fe.getCodes()` if you want Spring-managed messages/locale. 4. **Multiple violations per field** — decide whether to return all or first; `getFieldErrors()` returns them all. 5. **Ordering / determinism** — validators don't guarantee order; sort if the client needs stability. 6. **Don't echo rejected values blindly** — for sensitive fields (passwords) omit `getRejectedValue()` to avoid leaking secrets in logs/responses. ## When to use in-method BindingResult instead Only for server-rendered forms where you re-display the page. For JSON APIs, centralized advice keeps controllers thin and the contract uniform.

  • Why won't your MethodArgumentNotValidException handler catch a bad @RequestParam value?
    @RequestParam/@PathVariable constraints run via method-level validation and throw ConstraintViolationException (or HandlerMethodValidationException), not MethodArgumentNotValidException. You need a separate handler.
  • What is the modern Spring way to standardize error bodies?
    RFC 7807 ProblemDetail. Extend ResponseEntityExceptionHandler and override handleMethodArgumentNotValid, enriching the pre-built ProblemDetail (ex.getBody()) with an errors property.

saying these in an interview costs you the question

  • Using BindingResult in every REST controller instead of centralized advice
  • Assuming one handler for MethodArgumentNotValidException also catches @RequestParam violations
  • Returning the framework's default error body as the API contract
  • Echoing rejected values for sensitive fields like passwords

context