Allow Jackson features to be configured via the environment

Enhance JacksonAutoConfiguration to configure features on the
ObjectMapper it creates based on the following configuration
properties:

spring.jackson.deserialization.* = true|false
spring.jackson.generator.* = true|false
spring.jackson.mapper.* = true|false
spring.jackson.parser.* = true|false
spring.jackson.serialization.* = true|false

The final part of each property name maps onto an enum. The enums are:

deserialization: com.fasterxml.jackson.databind.DeserializationFeature
generator: com.fasterxml.jackson.core.JsonGenerator.Feature
mapper: com.fasterxml.jackson.databind.MapperFeature
parser: com.fasterxml.jackson.core.JsonParser.Feature
serialization: com.fasterxml.jackson.databind.SerializationFeature

Closes gh-1227
pull/1652/head
Andy Wilkinson 10 years ago
parent 26ac68df05
commit 4b25b0e7a2

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.jackson; package org.springframework.boot.autoconfigure.jackson;
import java.util.Collection; import java.util.Collection;
import java.util.Map.Entry;
import javax.annotation.PostConstruct; 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.Configuration;
import org.springframework.context.annotation.Primary; 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.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
@ -51,6 +56,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
* </ul> * </ul>
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Andy Wilkinson
* @since 1.1.0 * @since 1.1.0
*/ */
@Configuration @Configuration
@ -75,24 +81,73 @@ public class JacksonAutoConfiguration {
@Configuration @Configuration
@ConditionalOnClass(ObjectMapper.class) @ConditionalOnClass(ObjectMapper.class)
@EnableConfigurationProperties(HttpMapperProperties.class) @EnableConfigurationProperties({ HttpMapperProperties.class, JacksonProperties.class })
static class JacksonObjectMapperAutoConfiguration { static class JacksonObjectMapperAutoConfiguration {
@Autowired @Autowired
private HttpMapperProperties properties = new HttpMapperProperties(); private HttpMapperProperties httpMapperProperties = new HttpMapperProperties();
@Autowired
private JacksonProperties jacksonProperties = new JacksonProperties();
@Bean @Bean
@Primary @Primary
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper() { public ObjectMapper jacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
if (this.properties.isJsonSortKeys()) {
if (this.httpMapperProperties.isJsonSortKeys()) {
objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS,
true); true);
} }
configureDeserializationFeatures(objectMapper);
configureSerializationFeatures(objectMapper);
configureMapperFeatures(objectMapper);
configureParserFeatures(objectMapper);
configureGeneratorFeatures(objectMapper);
return objectMapper; return objectMapper;
} }
private void configureDeserializationFeatures(ObjectMapper objectMapper) {
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
.getDeserialization().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}
private void configureSerializationFeatures(ObjectMapper objectMapper) {
for (Entry<SerializationFeature, Boolean> entry : this.jacksonProperties
.getSerialization().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}
private void configureMapperFeatures(ObjectMapper objectMapper) {
for (Entry<MapperFeature, Boolean> entry : this.jacksonProperties.getMapper()
.entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}
private void configureParserFeatures(ObjectMapper objectMapper) {
for (Entry<JsonParser.Feature, Boolean> entry : this.jacksonProperties
.getParser().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}
private void configureGeneratorFeatures(ObjectMapper objectMapper) {
for (Entry<JsonGenerator.Feature, Boolean> entry : this.jacksonProperties
.getGenerator().entrySet()) {
objectMapper.configure(entry.getKey(), isFeatureEnabled(entry));
}
}
private boolean isFeatureEnabled(Entry<?, Boolean> entry) {
return entry.getValue() != null && entry.getValue();
}
} }
@Configuration @Configuration

@ -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<SerializationFeature, Boolean> serialization = new HashMap<SerializationFeature, Boolean>();
private Map<DeserializationFeature, Boolean> deserialization = new HashMap<DeserializationFeature, Boolean>();
private Map<MapperFeature, Boolean> mapper = new HashMap<MapperFeature, Boolean>();
private Map<JsonParser.Feature, Boolean> parser = new HashMap<JsonParser.Feature, Boolean>();
private Map<JsonGenerator.Feature, Boolean> generator = new HashMap<JsonGenerator.Feature, Boolean>();
public Map<SerializationFeature, Boolean> getSerialization() {
return this.serialization;
}
public Map<DeserializationFeature, Boolean> getDeserialization() {
return this.deserialization;
}
public Map<MapperFeature, Boolean> getMapper() {
return this.mapper;
}
public Map<JsonParser.Feature, Boolean> getParser() {
return this.parser;
}
public Map<JsonGenerator.Feature, Boolean> getGenerator() {
return this.generator;
}
}

@ -25,16 +25,21 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule; 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.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -101,6 +108,128 @@ public class JacksonAutoConfigurationTests {
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo())); 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 @Configuration
protected static class ModulesConfig { protected static class ModulesConfig {

@ -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.cache-period= # cache timeouts in headers sent to browser
spring.resources.add-mappings=true # if default mappings should be added 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]) # THYMELEAF ({sc-spring-boot-autoconfigure}/thymeleaf/ThymeleafAutoConfiguration.{sc-ext}[ThymeleafAutoConfiguration])
spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html spring.thymeleaf.suffix=.html

@ -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 converter with a vanilla `ObjectMapper`. Spring Boot has some features to make it easier
to customize this behavior. to customize this behavior.
The smallest change that might work is to just add beans of type You can configure the vanilla `ObjectMapper` using the environment. Jackson provides an
`com.fasterxml.jackson.databind.Module` to your context. They will be registered with the extensive suite of simple on/off features that can be used to configure various aspects
default `ObjectMapper` and then injected into the default message converter. To replace of its processing. These features are described in five enums in Jackson which map onto
the default `ObjectMapper` completely, define a `@Bean` of that type and mark it as properties in the environment:
`@Primary`.
|===
In addition, if your context contains any beans of type `ObjectMapper` then all of the |Jackson enum|Environment property
`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. |`com.fasterxml.jackson.databind.DeserializationFeature`
|`spring.jackson.deserialization.<feature_name>=true\|false`
|`com.fasterxml.jackson.core.JsonGenerator.Feature`
|`spring.jackson.generator.<feature_name>=true\|false`
|`com.fasterxml.jackson.databind.MapperFeature`
|`spring.jackson.mapper.<feature_name>=true\|false`
|`com.fasterxml.jackson.core.JsonParser.Feature`
|`spring.jackson.parser.<feature_name>=true\|false`
|`com.fasterxml.jackson.databind.SerializationFeature`
|`spring.jackson.serialization.<feature_name>=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 <<boot-features-external-config-relaxed-binding, relaxed binding>>,
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 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 they will replace the default value in the MVC configuration. Also, a convenience bean is

Loading…
Cancel
Save