diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index 8942727975771..d0e645f412f5e 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -1415,9 +1415,28 @@ In both cases, importing those modules will allow HTTP message bodies to be read and serialised to JSON, for <>. -==== Advanced Jackson-specific features +==== Jackson-specific features -When using the `quarkus-rest-jackson` extension there are some advanced features that Quarkus REST supports. +===== Exception handling + +By default, Quarkus provides a built-in `ExceptionMapper` for `MismatchedInputException` which returns an HTTP 400 status code +along with a good error message in Dev and Test modes, about what went wrong during serialization of an entity. + +[NOTE] +==== +There are situations where various Jackson related exceptions need to handled in a uniform way.For example, the application may need to handle all `JsonMappingException` the same way. +This becomes a problem when taking JAX-RS / Jakarta REST rules into account, because the exception mapper `ExceptionMapper` for `MismatchedInputException` would be used instead of the user provide +`ExceptionMapper` for `JsonMappingException` (as `MismatchedInputException` is a subtype of `JsonMappingException`). + +One solution for this case is to configure the following: + +[source,properties] +---- +quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class +---- + +which essentially makes Quarkus ignore the `ExceptionMapper` for `MismatchedInputException` completely. +==== [[secure-serialization]] ===== Secure serialization diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index 9fa08c45b54d2..c2a0ec0735be8 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -72,7 +72,6 @@ import io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization; import io.quarkus.resteasy.reactive.jackson.SecureField; import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; -import io.quarkus.resteasy.reactive.jackson.runtime.mappers.DefaultMismatchedInputException; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.NativeInvalidDefinitionExceptionMapper; import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; import io.quarkus.resteasy.reactive.jackson.runtime.security.SecurityCustomSerialization; @@ -108,6 +107,7 @@ public class ResteasyReactiveJacksonProcessor { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final List HANDLED_MEDIA_TYPES = List.of(MediaType.APPLICATION_JSON, APPLICATION_NDJSON, APPLICATION_STREAM_JSON); + public static final String DEFAULT_MISMATCHED_INPUT_EXCEPTION = "io.quarkus.resteasy.reactive.jackson.runtime.mappers.BuiltinMismatchedInputExceptionMapper"; @BuildStep void feature(BuildProducer feature) { @@ -130,9 +130,16 @@ ReinitializeVertxJsonBuildItem vertxJson() { } @BuildStep - ExceptionMapperBuildItem exceptionMappers() { - return new ExceptionMapperBuildItem(DefaultMismatchedInputException.class.getName(), - MismatchedInputException.class.getName(), Priorities.USER + 100, false); + void exceptionMappers(BuildProducer producer) { + try { + Thread.currentThread().getContextClassLoader().loadClass(DEFAULT_MISMATCHED_INPUT_EXCEPTION); + } catch (NoClassDefFoundError | ClassNotFoundException e) { + // the class is not available, likely due to quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/DefaultMismatchedInputException.class + return; + } + + producer.produce(new ExceptionMapperBuildItem(DEFAULT_MISMATCHED_INPUT_EXCEPTION, + MismatchedInputException.class.getName(), Priorities.USER + 100, false)); } @BuildStep diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ExceptionInReaderWithExcludedBuiltInAndIncludedCustomMapperTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ExceptionInReaderWithExcludedBuiltInAndIncludedCustomMapperTest.java new file mode 100644 index 0000000000000..e334002585c07 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/ExceptionInReaderWithExcludedBuiltInAndIncludedCustomMapperTest.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import java.util.function.Supplier; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.DatabindException; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ExceptionInReaderWithExcludedBuiltInAndIncludedCustomMapperTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(FroMage.class, FroMageEndpoint.class, DatabindExceptionMapper.class); + } + }).overrideConfigKey("quarkus.class-loading.removed-resources.\"io.quarkus\\:quarkus-rest-jackson\"", + "io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class"); + + @Test + public void test() { + RestAssured.with().contentType("application/json").body("{\"name\": \"brie\"}").put("/fromage") + .then().statusCode(999); + } + + @Provider + public static class DatabindExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(DatabindException exception) { + return Response.status(999).build(); + } + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/DefaultMismatchedInputException.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.java similarity index 98% rename from extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/DefaultMismatchedInputException.java rename to extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.java index 54783b513b961..e9fe48c834fca 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/DefaultMismatchedInputException.java +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.java @@ -10,7 +10,8 @@ import io.quarkus.runtime.LaunchMode; -public class DefaultMismatchedInputException +@SuppressWarnings("unused") +public class BuiltinMismatchedInputExceptionMapper implements ExceptionMapper { @Override