skip to content

How do you customize a ProblemDetail — extension fields, type/instance URIs, and internationalized title/detail?

level: seniorimportance: should knowfreq 33%

answer

  1. setProperty → RFC 7807 extension members
  2. setType/setInstance/setTitle/setDetail
  3. MessageSource + problemDetail.<FQCN> codes for i18n
  4. override createProblemDetail / handleExceptionInternal
  5. never leak stack traces in detail

basics

~10 s

Use setProperty(name, value) for extra fields, setType/setInstance for URIs, and setTitle/setDetail for text. For i18n, configure a MessageSource and rely on ErrorResponse message codes (problemDetail.<ExceptionClass>) so Spring resolves localized title/detail.

solid answer

~30 s

A ProblemDetail is mutable: setType(URI), setTitle(String), setStatus(int), setDetail(String), setInstance(URI), and setProperty(name, value) for RFC 7807 extension members (correlation ids, field errors, timestamps). Extensions serialize as top-level JSON members. For localization, don't hardcode strings — configure a MessageSource (e.g. spring.messages.basename) and let the ErrorResponse contract resolve title/detail from codes problemDetail.title.<ExceptionClass> and problemDetail.<ExceptionClass>, with getDetailMessageArguments() supplying placeholders. To add cross-cutting fields to every framework-error body, extend ResponseEntityExceptionHandler and override createProblemDetail(...) or handleExceptionInternal(...). Keep type URIs stable and documented, and instance URIs unique per occurrence. Avoid leaking stack traces or internal details into detail.

code

java · 26 lines
java
@ControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(OrderValidationException.class)
    ProblemDetail handle(OrderValidationException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
                HttpStatus.BAD_REQUEST, "Order validation failed");
        pd.setType(URI.create("https://api.example.com/errors/validation"));
        pd.setProperty("errors", ex.getFieldErrors());   // extension member
        pd.setProperty("timestamp", java.time.Instant.now());
        return pd;
    }

    // Add a trace id to EVERY framework-error body
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, Object body, HttpHeaders headers,
            HttpStatusCode status, WebRequest request) {
        ResponseEntity<Object> re =
                super.handleExceptionInternal(ex, body, headers, status, request);
        if (re != null && re.getBody() instanceof ProblemDetail pd) {
            pd.setProperty("traceId", MDC.get("traceId"));
        }
        return re;
    }
}

go deeper

for a junior

Know setProperty adds extra fields and setTitle/setDetail set the text.

for a middle

Know type/instance URI semantics and that extensions are top-level JSON members.

for a senior

Explain MessageSource-based i18n via problemDetail codes and overriding handleExceptionInternal for cross-cutting fields.

for a principal

Govern type-URI stability as API contract and enforce no-internal-leakage for 5xx across the whole error surface.

## Setting the standard fields `ProblemDetail` exposes setters for each RFC 7807 field: ```java ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); pd.setType(URI.create("https://api.example.com/errors/validation")); pd.setTitle("Validation failed"); pd.setDetail("3 fields are invalid"); pd.setInstance(URI.create("/orders/42")); ``` - **type** — keep it a *stable*, documented URI; changing it is a breaking contract change. `about:blank` is fine when there is nothing to link to. - **instance** — should be unique to *this* occurrence (often the request path or a trace id URI). ## Extension members RFC 7807 permits arbitrary extra fields. `setProperty(String, Object)` adds them at the JSON top level: ```java pd.setProperty("timestamp", Instant.now()); pd.setProperty("traceId", tracer.currentTraceId()); pd.setProperty("errors", fieldErrorList); ``` They are stored in an internal properties map and serialized alongside `type`/`title`/etc. This is the standard place for validation error lists, correlation ids, and machine-readable error codes. ## Internationalization via MessageSource The robust way to localize `title`/`detail` is **not** to hardcode strings but to lean on the `ErrorResponse` contract's message codes: - Configure a `MessageSource` (Boot: `spring.messages.basename=messages`). - For an exception implementing `ErrorResponse`, `getDetailMessageCode()` defaults to `problemDetail.<FQCN>` and `getTitleMessageCode()` to `problemDetail.title.<FQCN>`. - Provide arguments via `getDetailMessageArguments()`. - At render time, `ResponseEntityExceptionHandler` invokes `ErrorResponse.updateAndGetBody(messageSource, locale)`, which replaces `detail`/`title` with the localized text. `messages_es.properties`: ``` problemDetail.com.example.OutOfCreditException=Saldo insuficiente: {0} problemDetail.title.com.example.OutOfCreditException=Sin crédito ``` Spring also predefines codes for its own framework exceptions, so you can localize built-in error messages the same way. ## Cross-cutting customization for framework errors To inject a field (say `traceId`) into *every* framework-exception body, extend `ResponseEntityExceptionHandler` and override: ```java @Override protected ResponseEntity<Object> handleExceptionInternal( Exception ex, Object body, HttpHeaders headers, HttpStatusCode status, WebRequest request) { ResponseEntity<Object> re = super.handleExceptionInternal(ex, body, headers, status, request); if (re.getBody() instanceof ProblemDetail pd) { pd.setProperty("traceId", currentTraceId()); } return re; } ``` You can also override `createProblemDetail(...)` to set common fields when the base class builds the body. ## Gotchas & security - **Don't leak internals**: `detail` should be safe for clients — never dump stack traces, SQL, or internal class names. For 5xx, use a generic detail and log the specifics server-side. - **type stability**: treat `type` URIs as part of your API contract. - **Jackson**: serialization uses the app's `ObjectMapper`; extension property values must be serializable (an `Instant` needs the JavaTimeModule, which Boot registers by default). - **status consistency**: if you `setStatus` on the body, ensure the actual HTTP status matches.

  • Where do extension fields set via setProperty appear in the JSON?
    As additional top-level members of the problem+json object, alongside the standard type/title/status/detail/instance fields.
  • How would you localize the detail text of a framework exception without writing custom handlers?
    Configure a MessageSource and add a message with the key problemDetail.<fully-qualified exception class>; Spring resolves it via the ErrorResponse message-code mechanism at render time.

saying these in an interview costs you the question

  • Putting stack traces or internal exception messages into the detail field of 5xx responses
  • Thinking extension fields nest under a 'properties' object in the JSON (they are top-level members)
  • Changing type URIs casually, breaking the client contract

context