How do you produce a clean JSON error response from MethodArgumentNotValidException for a REST API?
answer
- @RestControllerAdvice + @ExceptionHandler(MethodArgumentNotValidException)
- ex.getBindingResult().getFieldErrors()
- getField / getDefaultMessage / getRejectedValue
- Spring 6.1 ProblemDetail via ResponseEntityExceptionHandler
- also catch ConstraintViolationException + HttpMessageNotReadableException
basics
~10 sAdd 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 sFor 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@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
Know that a @ControllerAdvice/@ExceptionHandler can turn the exception into custom JSON.
Extract field errors from getBindingResult() and set status 400.
Cover ConstraintViolationException, HttpMessageNotReadableException, and ProblemDetail integration for a uniform contract.
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