Skip to content

Commit

Permalink
Merge pull request #13361 from gsmet/jackson-ignore
Browse files Browse the repository at this point in the history
Disable Jackson's FAIL_ON_UNKNOWN_PROPERTIES by default
  • Loading branch information
gsmet authored Nov 23, 2020
2 parents 2b4bafe + 542cb50 commit cd992fb
Show file tree
Hide file tree
Showing 15 changed files with 164 additions and 8 deletions.
9 changes: 8 additions & 1 deletion docs/src/main/asciidoc/rest-json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,14 @@ It will allow to narrow down the number of JAX-RS providers (which can be seen a

==== Jackson

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`
In Quarkus, the default Jackson `ObjectMapper` obtained via CDI (and consumed by the Quarkus extensions) is configured to ignore 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)`.

Also, 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.

An example where a custom module needs to be registered would look like so:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.pkg.steps.NativeBuild;
import io.quarkus.jackson.ObjectMapperProducer;
import io.quarkus.jackson.runtime.ObjectMapperProducer;
import io.quarkus.runtime.LaunchMode;

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import io.quarkus.funqy.deployment.FunctionInitializedBuildItem;
import io.quarkus.funqy.gcp.functions.FunqyCloudFunctionsBindingRecorder;
import io.quarkus.funqy.runtime.FunqyConfig;
import io.quarkus.jackson.ObjectMapperProducer;
import io.quarkus.jackson.runtime.ObjectMapperProducer;

public class FunqyCloudFunctionsBuildStep {
private static final String FEATURE_NAME = "funqy-google-cloud-functions";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import io.quarkus.funqy.deployment.FunctionBuildItem;
import io.quarkus.funqy.deployment.FunctionInitializedBuildItem;
import io.quarkus.funqy.runtime.bindings.http.FunqyHttpBindingRecorder;
import io.quarkus.jackson.ObjectMapperProducer;
import io.quarkus.jackson.runtime.ObjectMapperProducer;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import io.quarkus.funqy.runtime.FunqyConfig;
import io.quarkus.funqy.runtime.bindings.knative.events.FunqyKnativeEventsConfig;
import io.quarkus.funqy.runtime.bindings.knative.events.KnativeEventsBindingRecorder;
import io.quarkus.jackson.ObjectMapperProducer;
import io.quarkus.jackson.runtime.ObjectMapperProducer;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
Expand Down
5 changes: 5 additions & 0 deletions extensions/jackson/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,7 +46,10 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.jackson.ObjectMapperProducer;
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;

Expand Down Expand Up @@ -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<GeneratedBeanBuildItem> generatedBeans,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.jackson.fail-on-unknown-properties=true
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.jackson.runtime.ObjectMapperProducer;

/**
* Meant to be implemented by a CDI bean that provides arbitrary customization for the default {@link ObjectMapper}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* You can still override it locally with {@code @JsonIgnoreProperties(ignoreUnknown = false)}.
*/
@ConfigItem(defaultValue = "false")
public boolean failOnUnknownProperties;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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> jacksonConfigSupport(JacksonBuildTimeConfig jacksonBuildTimeConfig) {
return new Supplier<JacksonConfigSupport>() {

@Override
public JacksonConfigSupport get() {
return new JacksonConfigSupport(jacksonBuildTimeConfig.failOnUnknownProperties);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.jackson;
package io.quarkus.jackson.runtime;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -9,18 +9,25 @@
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;
import io.quarkus.jackson.ObjectMapperCustomizer;

@ApplicationScoped
public class ObjectMapperProducer {

@DefaultBean
@Singleton
@Produces
public ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> customizers) {
public ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> 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<ObjectMapperCustomizer> sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers);
for (ObjectMapperCustomizer customizer : sortedCustomizers) {
customizer.customize(objectMapper);
Expand Down

0 comments on commit cd992fb

Please sign in to comment.