Files
spring-boot-demo/problem-details/docs/03-validation-errors.md
T
asmhatreandClaude Opus 5 926250e1a9 Add problem-details: global exception handling with RFC 9457
Companion code for "Global Exception Handling with ProblemDetail (RFC 9457)
in Spring Boot 4". Thirteen failures under five handling setups (Boot
defaults, the Boot flag, a ResponseEntityExceptionHandler advice, advice
plus an ErrorController, a catch-all ordered first), validation errors,
i18n, content negotiation, Security's 401/403, silent 500s and decoding
on the client. 16 tests pin the behaviour.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:12:22 +00:00

63 lines
2.8 KiB
Markdown

# 3. Validation errors that say something
[← 2. Choosing a mechanism](02-choosing-a-mechanism.md) · [Index](../README.md) · Next: [4. Types and message codes →](04-i18n-and-types.md)
Two exceptions, depending on where the constraint is:
| Constraint on | Exception | Stock `detail` |
|---|---|---|
| a `@Valid @RequestBody` object | `MethodArgumentNotValidException` | `Invalid request content.` |
| a parameter directly (`@RequestParam @Max(100) int limit`) | `HandlerMethodValidationException` | `Validation failure` |
The second is Spring Framework 6.1's built-in method validation: a constraint annotation on a
controller parameter is enough, no `@Validated` on the class.
## The stock body tells the client nothing
From [`matrix-boot-flag.txt`](output/matrix-boot-flag.txt) - three violations in the request, none in
the response:
```
HTTP 400 Content-Type: application/problem+json
{"detail":"Invalid request content.","instance":"/orders","status":400,"title":"Bad Request"}
```
## Adding the violations
Override the protected hook, keep the body the base class built, add an extension member, and hand
it back to `handleExceptionInternal` so headers and status stay right
([`GlobalExceptionHandler`](../src/main/java/com/ankurm/problems/advice/GlobalExceptionHandler.java)):
```java
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatusCode status, WebRequest request) {
ProblemDetail pd = ex.getBody();
pd.setDetail("The request body has " + ex.getErrorCount() + " invalid field(s)");
pd.setProperty("errors", ex.getFieldErrors().stream()
.map(e -> Map.of("pointer", "#/" + e.getField().replace('.', '/'),
"detail", String.valueOf(e.getDefaultMessage())))
.toList());
return handleExceptionInternal(ex, pd, headers, status, request);
}
```
The `{"pointer","detail"}` shape is not invented: it is the example RFC 9457 itself uses in
section 3. From [`matrix-advice.txt`](output/matrix-advice.txt):
```
{"detail":"The request body has 3 invalid field(s)","instance":"/orders","status":400,"title":"Bad Request","errors":[{"pointer":"#/quantity","detail":"must be greater than or equal to 1"},{"pointer":"#/customerEmail","detail":"must be a well-formed email address"},{"pointer":"#/sku","detail":"must not be blank"}]}
```
## Do not assert on the order
The order in that line is not declaration order, and it differed between runs of the same
request. The contract
test asserts membership, not position.
## Nested fields
`FieldError.getField()` uses dots for nesting (`address.city`) and brackets for indexes
(`lines[0].sku`). The `replace('.', '/')` here handles the first; if your API has lists of objects,
convert `[n]` to `/n` as well so the pointer is a valid JSON Pointer.