Commit e070d554 authored by Marcel Overdijk's avatar Marcel Overdijk Committed by Andy Wilkinson

Make Jackson date format and property naming strategy configurable

Allow the environment to be used to configure Jackson's date format
and property naming strategy

Closes gh-1628
parent fcd855cd
......@@ -16,11 +16,15 @@
package org.springframework.boot.autoconfigure.jackson;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -33,6 +37,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
......@@ -40,6 +46,7 @@ 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.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
......@@ -57,6 +64,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
*
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @since 1.1.0
*/
@Configuration
......@@ -107,9 +115,65 @@ public class JacksonAutoConfiguration {
configureParserFeatures(objectMapper);
configureGeneratorFeatures(objectMapper);
configureDateFormat(objectMapper);
configurePropertyNamingStrategy(objectMapper);
return objectMapper;
}
private void configurePropertyNamingStrategy(ObjectMapper objectMapper) {
// We support a fully qualified class name extending Jackson's
// PropertyNamingStrategy or a string value corresponding to the constant
// names in PropertyNamingStrategy which hold default provided implementations
String propertyNamingStrategy = this.jacksonProperties
.getPropertyNamingStrategy();
if (propertyNamingStrategy != null) {
try {
Class<?> clazz = ClassUtils.forName(propertyNamingStrategy, null);
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) BeanUtils
.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
// Find the field (this way we automatically support new constants
// that may be added by Jackson in the future)
Field field = ReflectionUtils.findField(PropertyNamingStrategy.class,
propertyNamingStrategy, PropertyNamingStrategy.class);
if (field != null) {
try {
objectMapper
.setPropertyNamingStrategy((PropertyNamingStrategy) field
.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
else {
throw new IllegalArgumentException("Constant named '"
+ propertyNamingStrategy + "' not found on "
+ PropertyNamingStrategy.class.getName());
}
}
}
}
private void configureDateFormat(ObjectMapper objectMapper) {
// We support a fully qualified class name extending DateFormat or a date
// pattern string value
String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) {
try {
Class<?> clazz = ClassUtils.forName(dateFormat, null);
objectMapper.setDateFormat((DateFormat) BeanUtils
.instantiateClass(clazz));
}
catch (ClassNotFoundException e) {
objectMapper.setDateFormat(new SimpleDateFormat(dateFormat));
}
}
}
private void configureDeserializationFeatures(ObjectMapper objectMapper) {
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
.getDeserialization().entrySet()) {
......
......@@ -31,11 +31,16 @@ import com.fasterxml.jackson.databind.SerializationFeature;
* Configuration properties to configure Jackson
*
* @author Andy Wilkinson
* @author Marcel Overdijk
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.jackson")
public class JacksonProperties {
private String dateFormat;
private String propertyNamingStrategy;
private Map<SerializationFeature, Boolean> serialization = new HashMap<SerializationFeature, Boolean>();
private Map<DeserializationFeature, Boolean> deserialization = new HashMap<DeserializationFeature, Boolean>();
......@@ -46,6 +51,22 @@ public class JacksonProperties {
private Map<JsonGenerator.Feature, Boolean> generator = new HashMap<JsonGenerator.Feature, Boolean>();
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getPropertyNamingStrategy() {
return this.propertyNamingStrategy;
}
public void setPropertyNamingStrategy(String propertyNamingStrategy) {
this.propertyNamingStrategy = propertyNamingStrategy;
}
public Map<SerializationFeature, Boolean> getSerialization() {
return this.serialization;
}
......
......@@ -17,8 +17,11 @@
package org.springframework.boot.autoconfigure.jackson;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import org.junit.After;
import org.junit.Before;
......@@ -60,6 +63,8 @@ import static org.mockito.Mockito.verify;
*
* @author Dave Syer
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
*/
public class JacksonAutoConfigurationTests {
......@@ -108,6 +113,110 @@ public class JacksonAutoConfigurationTests {
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
}
/*
* ObjectMapper does not contain method to get the date format of the mapper. See
* https://github.com/FasterXML/jackson-databind/issues/559 If such a method will be
* provided below tests can be simplified.
*/
@Test
public void noCustomDateFormat() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals(String.valueOf(date.getTime()), mapper.writeValueAsString(date));
}
@Test
public void customDateFormat() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.date-format:yyyyMMddHHmmss");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals("\"19880625203000\"", mapper.writeValueAsString(date));
}
@Test
public void customDateFormatClass() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(
this.context,
"spring.jackson.date-format:org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationTests.MyDateFormat");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertEquals("\"1988-06-25 20:30:00\"", mapper.writeValueAsString(date));
}
public static class MyDateFormat extends SimpleDateFormat {
public MyDateFormat() {
super("yyyy-MM-dd HH:mm:ss");
}
}
/*
* ObjectMapper does not contain method to get the property naming strategy of the
* mapper. See https://github.com/FasterXML/jackson-databind/issues/559 If such a
* method will be provided below tests can be simplified.
*/
@Test
public void noCustomPropertyNamingStrategy() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"propertyName\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyCamelCaseToLowerCaseWithUnderscores()
throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyPascalCaseToCamelCase() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:PASCAL_CASE_TO_CAMEL_CASE");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"PropertyName\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyLowerCase() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.property-naming-strategy:LOWER_CASE");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"propertyname\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void customPropertyNamingStrategyClass() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
EnvironmentTestUtils
.addEnvironment(
this.context,
"spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy");
this.context.refresh();
ObjectMapper mapper = this.context.getBean(ObjectMapper.class);
assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar()));
}
@Test
public void enableSerializationFeature() throws Exception {
this.context.register(JacksonAutoConfiguration.class);
......@@ -297,4 +406,16 @@ public class JacksonAutoConfigurationTests {
}
protected static class Bar {
private String propertyName;
public String getPropertyName() {
return this.propertyName;
}
public void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}
}
}
......@@ -94,6 +94,8 @@ content into your application; rather pick only the properties that you need.
spring.resources.add-mappings=true # if default mappings should be added
# JACKSON ({sc-spring-boot-autoconfigure}}/jackson/JacksonProperties.{sc-ext}[JacksonProperties])
spring.jackson.date-format= # Date format string (e.g. yyyy-MM-dd HH:mm:ss), or a fully-qualified date format class name (e.g. com.fasterxml.jackson.databind.util.ISO8601DateFormat)
spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy (e.g. CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) or the fully-qualified class name of a PropertyNamingStrategy subclass
spring.jackson.deserialization.*= # see Jackson's DeserializationFeature
spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature
spring.jackson.mapper.*= # see Jackson's MapperFeature
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment