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
This commit is contained in:
Andy Wilkinson
2014-08-06 17:08:21 +01:00
parent 26ac68df05
commit 4b25b0e7a2
5 changed files with 298 additions and 11 deletions

View File

@@ -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;
* </ul>
*
* @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<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

View File

@@ -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;
}
}

View File

@@ -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 {