diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index 9163f3840ecfb8..66452bed7af5cb 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -186,6 +186,11 @@ It will allow to narrow down the number of JAX-RS providers (which can be seen a ==== Jackson +In Quarkus, Jackson is configured to ignore the unknown properties (by disabling the `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` feature). + +You can restore the default behavior of Jackson by setting `quarkus.jackson.fail-on-unknown-properties=true` in your `application.properties` +or on a per class basis via `@JsonIgnoreProperties(ignoreUnknown = false)`. + Quarkus makes it very easy to configure various Jackson settings via CDI beans. The simplest (and suggested) approach is to define a CDI bean of type `io.quarkus.jackson.ObjectMapperCustomizer` inside of which any Jackson configuration can be applied. diff --git a/extensions/jackson/deployment/pom.xml b/extensions/jackson/deployment/pom.xml index ad626f20c32d2e..463bf956f5c579 100644 --- a/extensions/jackson/deployment/pom.xml +++ b/extensions/jackson/deployment/pom.xml @@ -30,6 +30,11 @@ io.quarkus quarkus-jackson + + io.quarkus + quarkus-junit5-internal + test + diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index 094f4b2e5a2830..0d55b67eb7e8fe 100755 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -29,9 +29,12 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CapabilityBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -43,6 +46,9 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.jackson.ObjectMapperCustomizer; +import io.quarkus.jackson.runtime.JacksonBuildTimeConfig; +import io.quarkus.jackson.runtime.JacksonConfigSupport; +import io.quarkus.jackson.runtime.JacksonRecorder; import io.quarkus.jackson.runtime.ObjectMapperProducer; import io.quarkus.jackson.spi.ClassPathJacksonModuleBuildItem; import io.quarkus.jackson.spi.JacksonModuleBuildItem; @@ -168,6 +174,16 @@ private void registerModuleIfOnClassPath(String moduleClassName, } } + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + SyntheticBeanBuildItem pushConfigurationBean(JacksonRecorder jacksonRecorder, + JacksonBuildTimeConfig jacksonBuildTimeConfig) { + return SyntheticBeanBuildItem.configure(JacksonConfigSupport.class) + .scope(Singleton.class) + .supplier(jacksonRecorder.jacksonConfigSupport(jacksonBuildTimeConfig)) + .done(); + } + // Generate a ObjectMapperCustomizer bean that registers each serializer / deserializer as well as detected modules with the ObjectMapper @BuildStep void generateCustomizer(BuildProducer generatedBeans, diff --git a/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonFailOnUnknownPropertiesTest.java b/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonFailOnUnknownPropertiesTest.java new file mode 100644 index 00000000000000..039fbb0d9dcd99 --- /dev/null +++ b/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonFailOnUnknownPropertiesTest.java @@ -0,0 +1,35 @@ +package io.quarkus.jackson.deployment; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; + +import io.quarkus.test.QuarkusUnitTest; + +public class JacksonFailOnUnknownPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-fail-on-unknown-properties.properties"); + + @Inject + ObjectMapper objectMapper; + + @Test + public void testFailOnUnknownProperties() throws JsonMappingException, JsonProcessingException { + Assertions.assertThrows(UnrecognizedPropertyException.class, + () -> objectMapper.readValue("{\"property\": \"name\", \"unknownProperty\": \"unknown\"}", Pojo.class)); + } + + public static class Pojo { + + public String property; + } +} diff --git a/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonIgnoreUnknownPropertiesTest.java b/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonIgnoreUnknownPropertiesTest.java new file mode 100644 index 00000000000000..b99971101e37f9 --- /dev/null +++ b/extensions/jackson/deployment/src/test/java/io/quarkus/jackson/deployment/JacksonIgnoreUnknownPropertiesTest.java @@ -0,0 +1,34 @@ +package io.quarkus.jackson.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.test.QuarkusUnitTest; + +public class JacksonIgnoreUnknownPropertiesTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest(); + + @Inject + ObjectMapper objectMapper; + + @Test + public void testIgnoreUnknownProperties() throws JsonMappingException, JsonProcessingException { + Pojo pojo = objectMapper.readValue("{\"property\": \"name\", \"unknownProperty\": \"unknown\"}", Pojo.class); + assertEquals("name", pojo.property); + } + + public static class Pojo { + + public String property; + } +} diff --git a/extensions/jackson/deployment/src/test/resources/application-fail-on-unknown-properties.properties b/extensions/jackson/deployment/src/test/resources/application-fail-on-unknown-properties.properties new file mode 100644 index 00000000000000..7fc657a94830f8 --- /dev/null +++ b/extensions/jackson/deployment/src/test/resources/application-fail-on-unknown-properties.properties @@ -0,0 +1 @@ +quarkus.jackson.fail-on-unknown-properties=true \ No newline at end of file diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonBuildTimeConfig.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonBuildTimeConfig.java new file mode 100644 index 00000000000000..d1b09e49614aa1 --- /dev/null +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonBuildTimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.jackson.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class JacksonBuildTimeConfig { + + /** + * If enabled, Jackson will fail when encountering unknown properties. + *

+ * You can still override it locally with {@code @JsonIgnoreProperties(ignoreUnknown = false)}. + */ + @ConfigItem(defaultValue = "false") + public boolean failOnUnknownProperties; +} diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonConfigSupport.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonConfigSupport.java new file mode 100644 index 00000000000000..e4a51fa2f31ce5 --- /dev/null +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonConfigSupport.java @@ -0,0 +1,14 @@ +package io.quarkus.jackson.runtime; + +public class JacksonConfigSupport { + + private boolean failOnUnknownProperties; + + public JacksonConfigSupport(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + public boolean isFailOnUnknownProperties() { + return failOnUnknownProperties; + } +} diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonRecorder.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonRecorder.java new file mode 100644 index 00000000000000..44c29b235d6f9a --- /dev/null +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/JacksonRecorder.java @@ -0,0 +1,19 @@ +package io.quarkus.jackson.runtime; + +import java.util.function.Supplier; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class JacksonRecorder { + + public Supplier jacksonConfigSupport(JacksonBuildTimeConfig jacksonBuildTimeConfig) { + return new Supplier() { + + @Override + public JacksonConfigSupport get() { + return new JacksonConfigSupport(jacksonBuildTimeConfig.failOnUnknownProperties); + } + }; + } +} diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/ObjectMapperProducer.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/ObjectMapperProducer.java index e10c27ade6716f..ad99e4010253c5 100644 --- a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/ObjectMapperProducer.java +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/runtime/ObjectMapperProducer.java @@ -9,6 +9,7 @@ import javax.enterprise.inject.Produces; import javax.inject.Singleton; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.arc.DefaultBean; @@ -20,8 +21,13 @@ public class ObjectMapperProducer { @DefaultBean @Singleton @Produces - public ObjectMapper objectMapper(Instance customizers) { + public ObjectMapper objectMapper(Instance customizers, + JacksonConfigSupport jacksonConfigSupport) { ObjectMapper objectMapper = new ObjectMapper(); + if (!jacksonConfigSupport.isFailOnUnknownProperties()) { + // this feature is enabled by default, so we disable it + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } List sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers); for (ObjectMapperCustomizer customizer : sortedCustomizers) { customizer.customize(objectMapper);