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
2.8 KiB
3. Validation errors that say something
← 2. Choosing a mechanism · Index · Next: 4. Types and message codes →
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 - 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):
@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:
{"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.