diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index d8642c8f0b..0453d5e8fd 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.jackson; import java.util.Collection; +import java.util.Map.Entry; import javax.annotation.PostConstruct; @@ -33,6 +34,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -51,6 +56,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module; * * * @author Oliver Gierke + * @author Andy Wilkinson * @since 1.1.0 */ @Configuration @@ -75,24 +81,73 @@ public class JacksonAutoConfiguration { @Configuration @ConditionalOnClass(ObjectMapper.class) - @EnableConfigurationProperties(HttpMapperProperties.class) + @EnableConfigurationProperties({ HttpMapperProperties.class, JacksonProperties.class }) static class JacksonObjectMapperAutoConfiguration { @Autowired - private HttpMapperProperties properties = new HttpMapperProperties(); + private HttpMapperProperties httpMapperProperties = new HttpMapperProperties(); + + @Autowired + private JacksonProperties jacksonProperties = new JacksonProperties(); @Bean @Primary @ConditionalOnMissingBean public ObjectMapper jacksonObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); - if (this.properties.isJsonSortKeys()) { + + if (this.httpMapperProperties.isJsonSortKeys()) { objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); } + + configureDeserializationFeatures(objectMapper); + configureSerializationFeatures(objectMapper); + configureMapperFeatures(objectMapper); + configureParserFeatures(objectMapper); + configureGeneratorFeatures(objectMapper); + return objectMapper; } + private void configureDeserializationFeatures(ObjectMapper objectMapper) { + for (Entry entry : this.jacksonProperties + .getDeserialization().entrySet()) { + objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); + } + } + + private void configureSerializationFeatures(ObjectMapper objectMapper) { + for (Entry entry : this.jacksonProperties + .getSerialization().entrySet()) { + objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); + } + } + + private void configureMapperFeatures(ObjectMapper objectMapper) { + for (Entry entry : this.jacksonProperties.getMapper() + .entrySet()) { + objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); + } + } + + private void configureParserFeatures(ObjectMapper objectMapper) { + for (Entry entry : this.jacksonProperties + .getParser().entrySet()) { + objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); + } + } + + private void configureGeneratorFeatures(ObjectMapper objectMapper) { + for (Entry entry : this.jacksonProperties + .getGenerator().entrySet()) { + objectMapper.configure(entry.getKey(), isFeatureEnabled(entry)); + } + } + + private boolean isFeatureEnabled(Entry entry) { + return entry.getValue() != null && entry.getValue(); + } } @Configuration diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java new file mode 100644 index 0000000000..327a2f10d4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jackson; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Configuration properties to configure Jackson + * + * @author Andy Wilkinson + */ +@ConfigurationProperties(prefix = "spring.jackson") +public class JacksonProperties { + + private Map serialization = new HashMap(); + + private Map deserialization = new HashMap(); + + private Map mapper = new HashMap(); + + private Map parser = new HashMap(); + + private Map generator = new HashMap(); + + public Map getSerialization() { + return this.serialization; + } + + public Map getDeserialization() { + return this.deserialization; + } + + public Map getMapper() { + return this.mapper; + } + + public Map getParser() { + return this.parser; + } + + public Map getGenerator() { + return this.generator; + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 6cbe098ea3..e0aa296634 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -25,16 +25,21 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.joda.JodaModule; @@ -44,7 +49,9 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.verify; @@ -101,6 +108,128 @@ public class JacksonAutoConfigurationTests { assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo())); } + @Test + public void enableSerializationFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.serialization.indent_output:true"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertFalse(SerializationFeature.INDENT_OUTPUT.enabledByDefault()); + assertTrue(mapper.getSerializationConfig().hasSerializationFeatures( + SerializationFeature.INDENT_OUTPUT.getMask())); + } + + @Test + public void disableSerializationFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.serialization.write_dates_as_timestamps:false"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertTrue(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.enabledByDefault()); + assertFalse(mapper.getSerializationConfig().hasSerializationFeatures( + SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.getMask())); + } + + @Test + public void enableDeserializationFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.deserialization.use_big_decimal_for_floats:true"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertFalse(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.enabledByDefault()); + assertTrue(mapper.getDeserializationConfig().hasDeserializationFeatures( + DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.getMask())); + } + + @Test + public void disableDeserializationFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.deserialization.fail_on_unknown_properties:false"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertTrue(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()); + assertFalse(mapper.getDeserializationConfig().hasDeserializationFeatures( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.getMask())); + } + + @Test + public void enableMapperFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.mapper.require_setters_for_getters:true"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertFalse(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.enabledByDefault()); + assertTrue(mapper.getSerializationConfig().hasMapperFeatures( + MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.getMask())); + assertTrue(mapper.getDeserializationConfig().hasMapperFeatures( + MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.getMask())); + } + + @Test + public void disableMapperFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.mapper.use_annotations:false"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertTrue(MapperFeature.USE_ANNOTATIONS.enabledByDefault()); + assertFalse(mapper.getDeserializationConfig().hasMapperFeatures( + MapperFeature.USE_ANNOTATIONS.getMask())); + assertFalse(mapper.getSerializationConfig().hasMapperFeatures( + MapperFeature.USE_ANNOTATIONS.getMask())); + } + + @Test + public void enableParserFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.parser.allow_single_quotes:true"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertFalse(JsonParser.Feature.ALLOW_SINGLE_QUOTES.enabledByDefault()); + assertTrue(mapper.getFactory().isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES)); + } + + @Test + public void disableParserFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.parser.auto_close_source:false"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertTrue(JsonParser.Feature.AUTO_CLOSE_SOURCE.enabledByDefault()); + assertFalse(mapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)); + } + + @Test + public void enableGeneratorFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.generator.write_numbers_as_strings:true"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertFalse(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS.enabledByDefault()); + assertTrue(mapper.getFactory().isEnabled( + JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)); + } + + @Test + public void disableGeneratorFeature() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.generator.auto_close_target:false"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertTrue(JsonGenerator.Feature.AUTO_CLOSE_TARGET.enabledByDefault()); + assertFalse(mapper.getFactory() + .isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET)); + } + @Configuration protected static class ModulesConfig { diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 32c7f1333c..1b643a93db 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -88,6 +88,13 @@ content into your application; rather pick only the properties that you need. spring.resources.cache-period= # cache timeouts in headers sent to browser spring.resources.add-mappings=true # if default mappings should be added + # JACKSON ({sc-spring-boot-autoconfigure}}/jackson/JacksonProperties.{sc-ext}[JacksonProperties]) + spring.jackson.deserialization.*= # see Jackson's DeserializationFeature + spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature + spring.jackson.mapper.*= # see Jackson's MapperFeature + spring.jackson.parser.*= # see Jackson's JsonParser.Feature + spring.jackson.serialization.*= # see Jackson's SerializationFeature + # THYMELEAF ({sc-spring-boot-autoconfigure}/thymeleaf/ThymeleafAutoConfiguration.{sc-ext}[ThymeleafAutoConfiguration]) spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 5c0745b619..4d3e3e1362 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -654,15 +654,43 @@ conversion in an HTTP exchange. If Jackson is on the classpath you already get a converter with a vanilla `ObjectMapper`. Spring Boot has some features to make it easier to customize this behavior. -The smallest change that might work is to just add beans of type -`com.fasterxml.jackson.databind.Module` to your context. They will be registered with the -default `ObjectMapper` and then injected into the default message converter. To replace -the default `ObjectMapper` completely, define a `@Bean` of that type and mark it as -`@Primary`. - -In addition, if your context contains any beans of type `ObjectMapper` then all of the -`Module` beans will be registered with all of the mappers. So there is a global mechanism -for contributing custom modules when you add new features to your application. +You can configure the vanilla `ObjectMapper` using the environment. Jackson provides an +extensive suite of simple on/off features that can be used to configure various aspects +of its processing. These features are described in five enums in Jackson which map onto +properties in the environment: + +|=== +|Jackson enum|Environment property + +|`com.fasterxml.jackson.databind.DeserializationFeature` +|`spring.jackson.deserialization.=true\|false` + +|`com.fasterxml.jackson.core.JsonGenerator.Feature` +|`spring.jackson.generator.=true\|false` + +|`com.fasterxml.jackson.databind.MapperFeature` +|`spring.jackson.mapper.=true\|false` + +|`com.fasterxml.jackson.core.JsonParser.Feature` +|`spring.jackson.parser.=true\|false` + +|`com.fasterxml.jackson.databind.SerializationFeature` +|`spring.jackson.serialization.=true\|false` +|=== + +For example, to allow deserialization to continue when an unknown property is encountered +during deserialization, set `spring.jackson.deserialization.fail_on_unknown_properties=false`. +Note that, thanks to the use of <>, +the case of `fail_on_unknown_properties` doesn't have to match the case of the corresponding +enum constant which is `FAIL_ON_UNKNOWN_PROPERTIES`. + +If you want to replace the default `ObjectMapper` completely, define a `@Bean` of that type +and mark it as `@Primary`. + +Another way to customize Jackson is to add beans of type +`com.fasterxml.jackson.databind.Module` to your context. They will be registered with every +bean of type `ObjectMapper`, providing a global mechanism for contributing custom modules +when you add new features to your application. Finally, if you provide any `@Beans` of type `MappingJackson2HttpMessageConverter` then they will replace the default value in the MVC configuration. Also, a convenience bean is