Validation Error Response Shape
Fast recall
Quick revision
Core answer
handleConstraintViolation for @Validated path/query params) in a @RestControllerAdvice extending ResponseEntityExceptionHandler. Build a `ProblemDetail` with status=400, a stable code=VALIDATION_FAILED, and an errors[] extension member containing one entry per field: {field, code, message, rejectedValue}. Collect all failures — never short-circuit on the first. Use a stable per-constraint code (NOT_BLANK, NOT_EMAIL, SIZE_EXCEEDED) sourced from FieldError.getCode() so the client can render per-field messages in its own locale. Apply the same shape across @Valid body failures, @Validated parameter failures, and Jackson deserialization issues so the client writes one handler.Whether you know which exceptions Bean Validation throws in each context (`@Valid` on @RequestBody, `@Validated` on path/query, class-level validation in services), how to translate them all into one field-level error shape, and why a stable per-constraint code matters for client UX.
Serializing `MethodArgumentNotValidException` directly — the client gets a huge Java-ish object with `target`, `codes`, `arguments`, `bindingResult`, and cryptic `NotBlank.customer.email` keys. Also: returning only the first error, forcing users to fix fields one at a time across multiple round-trips.
Discuss `ConstraintViolationException` vs `MethodArgumentNotValidException`, cross-field validation via `@AssertTrue`/class-level validators, `MessageSource` localization keyed by constraint code, `@Valid` nested collections (`List<@Valid ItemDto>`), and why the `rejectedValue` field matters (and when to scrub it for PII).
Interview-ready guide
Interview answer
Spring Bean Validation surfaces through three different exception types and my advice normalises them into one response shape.
The three exception types
MethodArgumentNotValidException—@Validon@RequestBodyor@ModelAttribute; has aBindingResultwithFieldErrorentriesHandlerMethodValidationException(Spring 6.1+) —@Validatedconstraints on controller method parameters like@RequestParam @Min(1) int pageConstraintViolationException—@Validatedon a service bean; raised from Hibernate Validator
Clients shouldn't care which one fired — they all produce the same errors[] array.
The response envelope
- RFC 7807
ProblemDetailwithstatus=400,title="Validation failed",code="VALIDATION_FAILED" - Extension member
errors— array of{field, code, message, rejectedValue}entries traceIdextension for correlation with APM and logs
The `code` convention
- Uppercase annotation simple name:
NotBlank→NOTBLANK,Size→SIZE,Pattern→PATTERN,Email→EMAIL - Stable enum clients branch on and localize against; message text is secondary
Field paths
FieldError.getField()returns dotted paths —address.street,phones[0].number- Preserve them; don't flatten; clients need them to highlight nested inputs
The `@RestControllerAdvice`
- Extends
ResponseEntityExceptionHandler - Overrides
handleMethodArgumentNotValidandhandleHandlerMethodValidationException - Separate
@ExceptionHandler(ConstraintViolationException.class)for service-layer failures - Each path builds the same
errors[]list and returns the sameProblemDetail
Collecting all failures
- Bean Validation runs every constraint by default — my advice never short-circuits on the first
- Short-circuiting forces users into N round-trips, one fix at a time; terrible UX
PII scrubbing
- I include
rejectedValuebut scrub emails, card numbers, and long strings - Raw values belong in audit logs, not in client responses
Common mistakes
- Serializing the raw
MethodArgumentNotValidException— Jackson dumps a huge Spring-internal blob - Using Spring Boot's default
{timestamp, status, error, path}instead of a structured shape - Treating Jackson deserialization errors as validation — they throw earlier as
HttpMessageNotReadableExceptionand should get their owncode: BODY_MALFORMED - Short-circuiting on first error — users rage-click
- Returning localized messages only — clients can't programmatically branch if the English version of the same constraint arrives the next day
Testing
- One MockMvc test per constraint; assert
Content-Type: application/problem+json, each expectederrors[].fieldanderrors[].code - New constraints added without tests are the classic regression
Validation Error Response Shape
Direct Answer
Override handleMethodArgumentNotValid (and handleConstraintViolation for @Validated path/query params) in a @RestControllerAdvice extending ResponseEntityExceptionHandler. Build a ProblemDetail with status=400, a stable code=VALIDATION_FAILED, and an errors[] extension member containing one entry per field: {field, code, message, rejectedValue}. Collect all failures — never short-circuit on the first. Use a stable per-constraint code (NOT_BLANK, NOT_EMAIL, SIZE_EXCEEDED) sourced from FieldError.getCode() so the client can render per-field messages in its own locale. Apply the same shape across @Valid body failures, @Validated parameter failures, and Jackson deserialization issues so the client writes one handler.
Code Example
public record CreateCustomerRequest(
@NotBlank(message = "{customer.name.required}")
@Size(max = 100)
String name,
@NotBlank
@Email(message = "{customer.email.invalid}")
String email,
@Past(message = "{customer.birthDate.inPast}")
LocalDate birthDate,
@NotNull
@Valid // cascade into the nested object
AddressDto address,
@Size(min = 1, max = 5)
List<@Valid PhoneDto> phones // cascade into each element
) {}
public record AddressDto(
@NotBlank String street,
@NotBlank @Size(min = 2, max = 2) String countryCode,
@Pattern(regexp = "\\d{5}(-\\d{4})?") String postalCode
) {}
// Controller — @Valid on the @RequestBody triggers MethodArgumentNotValidException
@RestController @RequestMapping("/v1/customers")
class CustomerController {
@PostMapping
CustomerDto create(@RequestBody @Valid CreateCustomerRequest req) {
return service.create(req);
}
// @Validated on path/query params -> HandlerMethodValidationException (Spring 6.1+)
@GetMapping
Page<CustomerDto> list(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size) {
return service.list(page, size);
}
}
Common Mistakes & Pitfalls
Serializing MethodArgumentNotValidException directly — the client gets a huge Java-ish object with target, codes, arguments, bindingResult, and cryptic NotBlank.customer.email keys. Also: returning only the first error, forcing users to fix fields one at a time across multiple round-trips.
Check your understanding
Answer these without looking back at the article.
- Why do `@Valid` on @RequestBody and `@Validated` on @RequestParam throw different exception types, and how do you unify the response?
- What breaks when you return `MethodArgumentNotValidException` directly as the response body?
- How does `@Valid` cascade into nested collections like `List<@Valid ItemDto>`?
- When should cross-field validation be a class-level `@AssertTrue` vs validation groups?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.