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:
		
							parent
							
								
									26ac68df05
								
							
						
					
					
						commit
						4b25b0e7a2
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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.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 { | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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`. | ||||
| 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: | ||||
| 
 | ||||
| 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. | ||||
| |=== | ||||
| |Jackson enum|Environment property | ||||
| 
 | ||||
| |`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 | ||||
| they will replace the default value in the MVC configuration. Also, a convenience bean is | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue