From 8068f2735ed6ca0dc266cf97242e7e8721d33230 Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Thu, 8 Jan 2026 11:28:49 +0100 Subject: [PATCH 1/2] Add name with JSON location to schemaViolation issue for JSON syntax error Fixes #277 --- .../problem/it/AbstractRestProblemIT.java | 2 +- .../spring/RoutingExceptionsHandler.java | 18 +++++++--- .../spring/RoutingExceptionsHandler.java | 18 +++++++--- .../AbstractRoutingExceptionsHandler.java | 35 +++++++++---------- .../AbstractRoutingExceptionsHandlerTest.java | 16 ++++++--- .../rest/problem/internal/Jackson2Util.java | 3 +- .../rest/problem/internal/Jackson3Util.java | 2 +- .../problem/internal/Jackson2UtilTest.java | 2 +- .../problem/internal/Jackson3UtilTest.java | 8 ++--- 9 files changed, 64 insertions(+), 40 deletions(-) diff --git a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java index 2cf8e54a..f30aae1f 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java +++ b/belgif-rest-problem-it/belgif-rest-problem-it-common/src/main/java/io/github/belgif/rest/problem/it/AbstractRestProblemIT.java @@ -542,7 +542,7 @@ public void invalidJsonNested() { .body("issues[0].title", equalTo("Input value is invalid with respect to the schema")) .body("issues[0].detail", equalTo("JSON syntax error")) .body("issues[0].in", equalTo("body")) - .body("issues[0].name", emptyOrNullString()); + .body("issues[0].name", equalTo("nested")); } @Test diff --git a/belgif-rest-problem-spring-boot-3/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java b/belgif-rest-problem-spring-boot-3/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java index f4b14928..0019577f 100644 --- a/belgif-rest-problem-spring-boot-3/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java +++ b/belgif-rest-problem-spring-boot-3/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java @@ -4,7 +4,9 @@ import org.springframework.core.annotation.Order; import org.springframework.web.bind.annotation.RestControllerAdvice; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.internal.Jackson2Util; @@ -17,15 +19,21 @@ @Order(1) // @Order(1) to take precedence over org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver // and io.github.belgif.rest.problem.spring.ProblemExceptionHandler -public class RoutingExceptionsHandler extends AbstractRoutingExceptionsHandler { +public class RoutingExceptionsHandler extends AbstractRoutingExceptionsHandler { public RoutingExceptionsHandler() { - super(MismatchedInputException.class); + super(JacksonException.class); } @Override - protected BadRequestProblem toBadRequestProblem(MismatchedInputException mismatchedInputException) { - return Jackson2Util.toBadRequestProblem(mismatchedInputException); + protected BadRequestProblem toBadRequestProblem(JacksonException jacksonException) { + if (jacksonException instanceof JsonMappingException jsonMappingException) { + return Jackson2Util.toBadRequestProblem(jsonMappingException); + } else if (jacksonException instanceof JsonParseException jsonParseException) { + return Jackson2Util.toBadRequestProblem(jsonParseException); + } else { + return null; + } } } diff --git a/belgif-rest-problem-spring-boot-4/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java b/belgif-rest-problem-spring-boot-4/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java index b472d476..4c3a4956 100644 --- a/belgif-rest-problem-spring-boot-4/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java +++ b/belgif-rest-problem-spring-boot-4/src/main/java/io/github/belgif/rest/problem/spring/RoutingExceptionsHandler.java @@ -6,7 +6,9 @@ import io.github.belgif.rest.problem.BadRequestProblem; import io.github.belgif.rest.problem.internal.Jackson3Util; -import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.core.JacksonException; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.DatabindException; /** * RestController exception handler for routing-related exceptions. @@ -16,15 +18,21 @@ @Order(1) // @Order(1) to take precedence over org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver // and io.github.belgif.rest.problem.spring.ProblemExceptionHandler -public class RoutingExceptionsHandler extends AbstractRoutingExceptionsHandler { +public class RoutingExceptionsHandler extends AbstractRoutingExceptionsHandler { public RoutingExceptionsHandler() { - super(MismatchedInputException.class); + super(JacksonException.class); } @Override - protected BadRequestProblem toBadRequestProblem(MismatchedInputException mismatchedInputException) { - return Jackson3Util.toBadRequestProblem(mismatchedInputException); + protected BadRequestProblem toBadRequestProblem(JacksonException jacksonException) { + if (jacksonException instanceof DatabindException databindException) { + return Jackson3Util.toBadRequestProblem(databindException); + } else if (jacksonException instanceof StreamReadException streamReadException) { + return Jackson3Util.toBadRequestProblem(streamReadException); + } else { + return null; + } } } diff --git a/belgif-rest-problem-spring-boot-common/src/main/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandler.java b/belgif-rest-problem-spring-boot-common/src/main/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandler.java index 51005291..cb0566f4 100644 --- a/belgif-rest-problem-spring-boot-common/src/main/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandler.java +++ b/belgif-rest-problem-spring-boot-common/src/main/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandler.java @@ -23,7 +23,7 @@ /** * RestController exception handler for routing-related exceptions. * - * @param the type of the Jackson MismatchedInputException + * @param the type of the Jackson exception */ public abstract class AbstractRoutingExceptionsHandler { @@ -31,10 +31,10 @@ public abstract class AbstractRoutingExceptionsHandler { private static final ProblemExceptionHandler DEFAULT_PROBLEM_EXCEPTION_HANDLER = new ProblemExceptionHandler(); - private final Class jacksonMismatchedInputExceptionClass; + private final Class jacksonExceptionClass; - protected AbstractRoutingExceptionsHandler(Class jacksonMismatchedInputExceptionClass) { - this.jacksonMismatchedInputExceptionClass = jacksonMismatchedInputExceptionClass; + protected AbstractRoutingExceptionsHandler(Class jacksonExceptionClass) { + this.jacksonExceptionClass = jacksonExceptionClass; } @ExceptionHandler(MissingServletRequestParameterException.class) @@ -59,28 +59,27 @@ public ResponseEntity handleMissingRequestHeaderException(MissingReques @ExceptionHandler(HttpMessageNotReadableException.class) @SuppressWarnings("unchecked") public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException exception) { - if (exception.getCause() != null - && jacksonMismatchedInputExceptionClass.isAssignableFrom(exception.getCause().getClass())) { - T mismatchedInputException = (T) exception.getCause(); - if (Arrays.stream(mismatchedInputException.getStackTrace()) + if (exception.getCause() != null && jacksonExceptionClass.isAssignableFrom(exception.getCause().getClass())) { + T jacksonException = (T) exception.getCause(); + if (Arrays.stream(jacksonException.getStackTrace()) .anyMatch(e -> e.getClassName().startsWith("org.springframework.web.client"))) { // When the MismatchedInputException originates from a REST Client API, it relates to an // invalid inbound response and should be mapped to HTTP 500 Internal Server Error return DEFAULT_PROBLEM_EXCEPTION_HANDLER.handleException(exception); - } else { - // Otherwise, it relates to an invalid inbound request and should be mapped to HTTP 400 Bad Request - return ProblemMediaType.INSTANCE.toResponse(toBadRequestProblem(mismatchedInputException)); } - } else { - LOGGER.info("Transforming HttpMessageNotReadableException " + - "to a BadRequestProblem with sanitized detail message", exception); - return ProblemMediaType.INSTANCE - .toResponse(new BadRequestProblem(InputValidationIssues.schemaViolation(InEnum.BODY, null, null, - getSanitizedProblemDetailMessage(exception)))); + BadRequestProblem problem = toBadRequestProblem(jacksonException); + if (problem != null) { + return ProblemMediaType.INSTANCE.toResponse(problem); + } } + LOGGER.info("Transforming HttpMessageNotReadableException " + + "to a BadRequestProblem with sanitized detail message", exception); + return ProblemMediaType.INSTANCE + .toResponse(new BadRequestProblem(InputValidationIssues.schemaViolation(InEnum.BODY, null, null, + getSanitizedProblemDetailMessage(exception)))); } - protected abstract BadRequestProblem toBadRequestProblem(T mismatchedInputException); + protected abstract BadRequestProblem toBadRequestProblem(T jacksonException); private String getSanitizedProblemDetailMessage(HttpMessageNotReadableException exception) { if (exception.getMessage() != null) { diff --git a/belgif-rest-problem-spring-boot-common/src/test/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandlerTest.java b/belgif-rest-problem-spring-boot-common/src/test/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandlerTest.java index 41cc2c12..e763a3a4 100644 --- a/belgif-rest-problem-spring-boot-common/src/test/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandlerTest.java +++ b/belgif-rest-problem-spring-boot-common/src/test/java/io/github/belgif/rest/problem/spring/AbstractRoutingExceptionsHandlerTest.java @@ -22,6 +22,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; @@ -33,11 +35,17 @@ class AbstractRoutingExceptionsHandlerTest { - private final AbstractRoutingExceptionsHandler handler = - new AbstractRoutingExceptionsHandler<>(MismatchedInputException.class) { + private final AbstractRoutingExceptionsHandler handler = + new AbstractRoutingExceptionsHandler<>(JacksonException.class) { @Override - protected BadRequestProblem toBadRequestProblem(MismatchedInputException mismatchedInputException) { - return Jackson2Util.toBadRequestProblem(mismatchedInputException); + protected BadRequestProblem toBadRequestProblem(JacksonException jacksonException) { + if (jacksonException instanceof JsonMappingException jsonMappingException) { + return Jackson2Util.toBadRequestProblem(jsonMappingException); + } else if (jacksonException instanceof JsonParseException jsonParseException) { + return Jackson2Util.toBadRequestProblem(jsonParseException); + } else { + return null; + } } }; diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java index 413839fb..4ee30822 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson2Util.java @@ -49,7 +49,8 @@ public static BadRequestProblem toBadRequestProblem(JsonParseException e) { */ public static BadRequestProblem toBadRequestProblem(JsonMappingException e) { if (e.getCause() instanceof JsonParseException) { - return toBadRequestProblem((JsonParseException) e.getCause()); + return new BadRequestProblem(schemaViolation(InEnum.BODY, getName(e.getPath()), + null, InputValidationIssues.DETAIL_JSON_SYNTAX_ERROR)); } else { return new BadRequestProblem( InputValidationIssues.schemaViolation(InEnum.BODY, getName(e.getPath()), diff --git a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java index 7555343f..b7942490 100644 --- a/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java +++ b/belgif-rest-problem/src/main/java/io/github/belgif/rest/problem/internal/Jackson3Util.java @@ -26,7 +26,7 @@ private Jackson3Util() { * @return the BadRequestProblem */ public static BadRequestProblem toBadRequestProblem(StreamReadException e) { - return new BadRequestProblem(schemaViolation(InEnum.BODY, null, + return new BadRequestProblem(schemaViolation(InEnum.BODY, getName(e.getPath()), null, InputValidationIssues.DETAIL_JSON_SYNTAX_ERROR)); } diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java index a32ce207..5bd62a86 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson2UtilTest.java @@ -90,7 +90,7 @@ void jsonParseExceptionWrappedJsonMappingException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isNull(); + assertThat(issue.getName()).isEqualTo("model"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("JSON syntax error"); }); diff --git a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java index f48b2d59..09cbf729 100644 --- a/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java +++ b/belgif-rest-problem/src/test/java/io/github/belgif/rest/problem/internal/Jackson3UtilTest.java @@ -40,7 +40,7 @@ void mismatchedInput() { @Test void mismatchedInputType() { assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(() -> { - new ObjectMapper().readValue("{ \"id\": \"123\", \"nbr\": \"twenty-two\" }", Jackson2UtilTest.Model.class); + new ObjectMapper().readValue("{ \"id\": \"123\", \"nbr\": \"twenty-two\" }", Model.class); }).satisfies(e -> { BadRequestProblem problem = Jackson3Util.toBadRequestProblem(e); InputValidationIssue issue = problem.getIssues().get(0); @@ -55,7 +55,7 @@ void mismatchedInputType() { @Test void valueInstantiationException() { assertThatExceptionOfType(ValueInstantiationException.class).isThrownBy(() -> { - new ObjectMapper().readValue("{ \"id\": \"123\", \"size\": \"XXL\" }", Jackson2UtilTest.Model.class); + new ObjectMapper().readValue("{ \"id\": \"123\", \"size\": \"XXL\" }", Model.class); }).satisfies(e -> { BadRequestProblem problem = Jackson3Util.toBadRequestProblem(e); InputValidationIssue issue = problem.getIssues().get(0); @@ -70,7 +70,7 @@ void valueInstantiationException() { @Test void invalidFormatException() { assertThatExceptionOfType(InvalidFormatException.class).isThrownBy(() -> { - new ObjectMapper().readValue("{ \"id\": \"123\", \"size2\": \"XXL\" }", Jackson2UtilTest.Model.class); + new ObjectMapper().readValue("{ \"id\": \"123\", \"size2\": \"XXL\" }", Model.class); }).satisfies(e -> { BadRequestProblem problem = Jackson3Util.toBadRequestProblem(e); InputValidationIssue issue = problem.getIssues().get(0); @@ -91,7 +91,7 @@ void streamReadException() { InputValidationIssue issue = problem.getIssues().get(0); assertThat(issue.getType()).hasToString("urn:problem-type:belgif:input-validation:schemaViolation"); assertThat(issue.getIn()).isEqualTo(InEnum.BODY); - assertThat(issue.getName()).isNull(); + assertThat(issue.getName()).isEqualTo("model"); assertThat(issue.getValue()).isNull(); assertThat(issue.getDetail()).isEqualTo("JSON syntax error"); }); From 5ff9b738bfb055d2cc8cd94a2c13e21764945fb4 Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Tue, 13 Jan 2026 08:48:28 +0100 Subject: [PATCH 2/2] Add release notes entry --- src/main/asciidoc/release-notes.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/asciidoc/release-notes.adoc b/src/main/asciidoc/release-notes.adoc index 36ef6a70..a98575d1 100644 --- a/src/main/asciidoc/release-notes.adoc +++ b/src/main/asciidoc/release-notes.adoc @@ -18,6 +18,7 @@ * Disable stacktrace on Problem exception class by default, as performance optimization. You can configure `io.github.belgif.rest.problem.stack-trace-enabled` = true to opt out of this. +* Add name with JSON location to schemaViolation issue for JSON syntax error *belgif-rest-problem-spring-boot-4:*