Files
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

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.