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
This commit is contained in:
parent
fcd855cd5e
commit
e070d55491
|
@ -16,11 +16,15 @@
|
||||||
|
|
||||||
package org.springframework.boot.autoconfigure.jackson;
|
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.Collection;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||||
import org.springframework.beans.factory.ListableBeanFactory;
|
import org.springframework.beans.factory.ListableBeanFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.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 org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
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.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.PropertyNamingStrategy;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
|
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
|
||||||
|
@ -57,6 +64,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
|
||||||
*
|
*
|
||||||
* @author Oliver Gierke
|
* @author Oliver Gierke
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Marcel Overdijk
|
||||||
* @since 1.1.0
|
* @since 1.1.0
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@ -107,9 +115,65 @@ public class JacksonAutoConfiguration {
|
||||||
configureParserFeatures(objectMapper);
|
configureParserFeatures(objectMapper);
|
||||||
configureGeneratorFeatures(objectMapper);
|
configureGeneratorFeatures(objectMapper);
|
||||||
|
|
||||||
|
configureDateFormat(objectMapper);
|
||||||
|
configurePropertyNamingStrategy(objectMapper);
|
||||||
|
|
||||||
return 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) {
|
private void configureDeserializationFeatures(ObjectMapper objectMapper) {
|
||||||
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
|
for (Entry<DeserializationFeature, Boolean> entry : this.jacksonProperties
|
||||||
.getDeserialization().entrySet()) {
|
.getDeserialization().entrySet()) {
|
||||||
|
|
|
@ -31,11 +31,16 @@ import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
* Configuration properties to configure Jackson
|
* Configuration properties to configure Jackson
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Marcel Overdijk
|
||||||
* @since 1.2.0
|
* @since 1.2.0
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "spring.jackson")
|
@ConfigurationProperties(prefix = "spring.jackson")
|
||||||
public class JacksonProperties {
|
public class JacksonProperties {
|
||||||
|
|
||||||
|
private String dateFormat;
|
||||||
|
|
||||||
|
private String propertyNamingStrategy;
|
||||||
|
|
||||||
private Map<SerializationFeature, Boolean> serialization = new HashMap<SerializationFeature, Boolean>();
|
private Map<SerializationFeature, Boolean> serialization = new HashMap<SerializationFeature, Boolean>();
|
||||||
|
|
||||||
private Map<DeserializationFeature, Boolean> deserialization = new HashMap<DeserializationFeature, 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>();
|
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() {
|
public Map<SerializationFeature, Boolean> getSerialization() {
|
||||||
return this.serialization;
|
return this.serialization;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
package org.springframework.boot.autoconfigure.jackson;
|
package org.springframework.boot.autoconfigure.jackson;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.LocalDateTime;
|
import org.joda.time.LocalDateTime;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
@ -60,6 +63,8 @@ import static org.mockito.Mockito.verify;
|
||||||
*
|
*
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
* @author Oliver Gierke
|
* @author Oliver Gierke
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
* @author Marcel Overdijk
|
||||||
*/
|
*/
|
||||||
public class JacksonAutoConfigurationTests {
|
public class JacksonAutoConfigurationTests {
|
||||||
|
|
||||||
|
@ -108,6 +113,110 @@ public class JacksonAutoConfigurationTests {
|
||||||
assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo()));
|
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
|
@Test
|
||||||
public void enableSerializationFeature() throws Exception {
|
public void enableSerializationFeature() throws Exception {
|
||||||
this.context.register(JacksonAutoConfiguration.class);
|
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
|
spring.resources.add-mappings=true # if default mappings should be added
|
||||||
|
|
||||||
# JACKSON ({sc-spring-boot-autoconfigure}}/jackson/JacksonProperties.{sc-ext}[JacksonProperties])
|
# 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.deserialization.*= # see Jackson's DeserializationFeature
|
||||||
spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature
|
spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature
|
||||||
spring.jackson.mapper.*= # see Jackson's MapperFeature
|
spring.jackson.mapper.*= # see Jackson's MapperFeature
|
||||||
|
|
Loading…
Reference in New Issue