spring.mvc.problemdetails.enabled
Makes Spring MVC's own exceptions return RFC 9457 Problem Details JSON (application/problem+json); it is off by default.
spring.mvc.problemdetails.enabled=true- Added in Spring Boot Spring Boot 4.1 (verified present)
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | Boolean= false | true to register the handler. It is only registered when the application has no ResponseEntityExceptionHandler bean of its own. |
$spring.webflux.problemdetails.enabledoptional | Boolean= false | The equivalent switch for WebFlux applications. |
Return value
n/aWhether Boot registers a ProblemDetailsExceptionHandler (a ResponseEntityExceptionHandler) so that Spring MVC exceptions are rendered as RFC 9457 Problem Details.
Errors
Exceptions that are not Spring MVC exceptions or ErrorResponse types still go to the /error fallback.
RFC 9457 defines a standard JSON shape for HTTP errors: type, title, status, detail and instance, sent as application/problem+json. Spring Framework has the types (ProblemDetail, ErrorResponse, ErrorResponseException), and this property switches Boot's use of them on for Spring MVC.
spring.mvc.problemdetails.enabled=trueWhat it covers
Boot registers a @ControllerAdvice that extends ResponseEntityExceptionHandler. That handler knows the exceptions Spring MVC itself raises: a body that fails @Valid, a missing request parameter, an unreadable JSON body, an unsupported media type, a method not allowed, a path with no handler. It also handles any ErrorResponseException, which includes ResponseStatusException. All of them come back as Problem Details instead of Boot's older error JSON with timestamp, error and path.
What it does not cover
Your own exceptions. An OrderNotFoundException extends RuntimeException still falls through to the /error fallback and its default body. Either make domain exceptions extend ErrorResponseException, or handle them in your own @RestControllerAdvice and return a ProblemDetail.
If you declare your own subclass of ResponseEntityExceptionHandler, Boot backs off and does not register its handler, whatever this property says. That is the usual approach once you want to customise the responses: extend the base class, override the methods you care about, and the property becomes irrelevant.
Why turn it on
Clients get one error format for framework and application errors, and generic HTTP tooling understands it. A validation failure returns 400 with a readable detail rather than a generic page. On a new API, enable it on the first day; changing the error format later breaks clients that parse it.
Keep the detail safe
The detail field is visible to callers. Write it for them, not for you: no stack traces, SQL, class names or internal hostnames. Log the full exception on the server with a correlation ID and return that ID in a custom property if support needs to find the log line.
Example
# Spring MVC exceptions become application/problem+json responses.
spring.mvc.problemdetails.enabled=true
When to use it
- Returning validation and malformed JSON errors in one documented format to mobile and web clients
- Replacing Boot's timestamp/error/path JSON with a standard that API gateways and SDKs understand
- Moving an existing API to Problem Details without writing a ControllerAdvice for framework errors
Frequently asked questions
Why do my own exceptions not return Problem Details after enabling the property?
The handler Boot registers only knows Spring MVC exceptions and ErrorResponse types. Extend ErrorResponseException or map your exceptions to ProblemDetail in a @RestControllerAdvice.
Does the property still matter if I have a ResponseEntityExceptionHandler subclass?
No. Boot only registers its own handler when no ResponseEntityExceptionHandler bean exists. Your subclass already produces Problem Details for the same exceptions.
More examples
A domain exception that renders as a problem
Because the exception extends ErrorResponseException, the handler Boot registers renders it with no extra code. A plain RuntimeException would bypass that handler and end up at /error.
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
class OrderNotFoundException extends ErrorResponseException {
OrderNotFoundException(long id) {
super(HttpStatus.NOT_FOUND, problem(id), null);
}
private static ProblemDetail problem(long id) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Order " + id + " does not exist.");
pd.setType(URI.create("https://api.example.com/problems/order-not-found"));
pd.setTitle("Order not found");
pd.setProperty("orderId", id);
return pd;
}
}
@RestController
public class OrderLookupController {
@GetMapping("/api/orders/{id}")
String find(@PathVariable long id) {
throw new OrderNotFoundException(id);
}
}
The response a client receives
A type conversion failure on a path variable is a Spring MVC exception, so it is rendered as a problem. about:blank is the RFC's type for problems that need no more explanation than the status code.
GET /api/orders/abc HTTP/1.1
Host: localhost:8080
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Failed to convert 'id' with value: 'abc'",
"instance": "/api/orders/abc"
}
Testing the error contract
The test pins the error format, so a refactor that adds a custom ResponseEntityExceptionHandler, or removes the property, shows up as a failing test rather than as a broken client.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class SearchController {
@GetMapping("/api/search")
String search(@RequestParam String q) {
return "results for " + q;
}
}
@WebMvcTest(controllers = SearchController.class, properties = "spring.mvc.problemdetails.enabled=true")
class SearchControllerProblemTest {
@Autowired
MockMvcTester mvc;
@Test
void missingParameterIsAProblem() {
assertThat(mvc.get().uri("/api/search"))
.hasStatus(HttpStatus.BAD_REQUEST)
.hasContentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)
.bodyJson()
.extractingPath("$.status")
.isEqualTo(400);
}
}
Discussion