Reputation: 25698
I'm trying to customize the Error Details response by including the validation errors. I'm running Spring Boot 3.4 and Java 21.
ProblemDetail::setProperty()
is supposed to do this according to the documentation:
When Jackson JSON is present on the classpath, any properties set here are rendered as top level key-value pairs in the output JSON. Otherwise, they are rendered as a "properties" sub-map.
I've added all the dependencies, I've doubled checked them in IntelliJ and Maven dependency:list
, they are there.
[INFO] com.fasterxml.jackson.core:jackson-databind:jar:2.18.1:compile -- module com.fasterxml.jackson.databind [INFO] com.fasterxml.jackson.core:jackson-core:jar:2.18.1:compile -- module com.fasterxml.jackson.core [INFO] com.fasterxml.jackson.core:jackson-annotations:jar:2.18.1:compile -- module com.fasterxml.jackson.annotation
My problem is, it doesn't work, it seems to fail to detect Jackson and keeps using properties
instead of adding it on the top level. Any ideas what I'm missing?
Maven Dependencies:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.18.1</version>
</dependency>
Generated JSON
Notice the properties
, the errors and timestamp should be on the top level, not below properties, according to the documentation.
{
"type":"http://localhost:8080/errors/bad-request",
"title":"Bad request",
"status":400,
"detail":null,
"instance":"/api/sign-up",
"properties":{
"timestamp":"2024-12-22T23:07:11.789380900Z",
"errors":[
{
"detail":"Password confirmation is required",
"pointer":"#/passwordConfirmation"
},
{
"detail":"Password is required",
"pointer":"#/password"
},
{
"detail":"Email is required",
"pointer":"#/email"
},
{
"detail":"Organization is required",
"pointer":"#/organization"
},
{
"detail":"First name is required",
"pointer":"#/firstName"
},
{
"detail":"Last name is required",
"pointer":"#/lastName"
}
]
}
}
Controller Signature:
@PostMapping( Routes.API_SIGN_UP)
@Transactional
public ResponseEntity<?> postSignUp(
@Validated @RequestBody UserRegistrationTransfer userRegistrationTransfer
) {
Exception Handler:
import java.net.URI;
import java.time.Instant;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.*;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request
) {
return ResponseEntity
.status(status.value())
.body(handleMethodArgumentNotValidException(ex));
}
private ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<Map<String, String>> details = getErrorsDetails(ex);
ProblemDetail problemDetail = ProblemDetail.forStatus(ex.getStatusCode());
problemDetail.setType(URI.create("http://localhost:8080/errors/bad-request"));
problemDetail.setTitle("Bad request");
problemDetail.setInstance(ex.getBody().getInstance());
// adding more data using the Map properties of the ProblemDetail
problemDetail.setProperty("timestamp", Instant.now().toString());
problemDetail.setProperty("errors", details);
return problemDetail;
}
private List<Map<String, String>> getErrorsDetails(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getAllErrors().stream()
.map(this::toErrorMap)
.toList();
}
private Map<String, String> toErrorMap(ObjectError error) {
Map<String, String> result = new LinkedHashMap<>();
result.put("detail", error.getDefaultMessage());
if (error instanceof FieldError fieldError) {
result.put("pointer", "#/" + fieldError.getField());
return result;
}
result.put("pointer", "#/");
return result;
}
}
Upvotes: 0
Views: 122