/* * Copyright 2012-2017 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.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.Set; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator.Mode; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.DeserializationConfig; 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.PropertyNamingStrategy.SnakeCaseStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.util.StdDateFormat; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.JsonComponent; import org.springframework.boot.jackson.JsonObjectSerializer; import org.springframework.boot.test.util.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; /** * Tests for {@link JacksonAutoConfiguration}. * * @author Dave Syer * @author Oliver Gierke * @author Andy Wilkinson * @author Marcel Overdijk * @author Sebastien Deleuze * @author Johannes Edmeier * @author Grzegorz Poznachowski */ public class JacksonAutoConfigurationTests { private AnnotationConfigApplicationContext context; @Before public void setUp() { this.context = new AnnotationConfigApplicationContext(); } @After public void tearDown() { if (this.context != null) { this.context.close(); } } @Test public void registersJodaModuleAutomatically() { this.context.register(JacksonAutoConfiguration.class); this.context.refresh(); ObjectMapper objectMapper = this.context.getBean(ObjectMapper.class); assertThat(objectMapper.canSerialize(LocalDateTime.class)).isTrue(); } @Test public void doubleModuleRegistration() throws Exception { this.context.register(DoubleModulesConfig.class, HttpMessageConvertersAutoConfiguration.class); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); assertThat(mapper.writeValueAsString(new Foo())).isEqualTo("{\"foo\":\"bar\"}"); } /* * 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); assertThat(mapper.getDateFormat()).isInstanceOf(StdDateFormat.class); } @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); DateFormat dateFormat = mapper.getDateFormat(); assertThat(dateFormat).isInstanceOf(SimpleDateFormat.class); assertThat(((SimpleDateFormat) dateFormat).toPattern()) .isEqualTo("yyyyMMddHHmmss"); } @Test public void customJodaDateTimeFormat() throws Exception { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.date-format:yyyyMMddHHmmss", "spring.jackson.joda-date-time-format:yyyy-MM-dd HH:mm:ss"); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); DateTime dateTime = new DateTime(1988, 6, 25, 20, 30, DateTimeZone.UTC); assertThat(mapper.writeValueAsString(dateTime)) .isEqualTo("\"1988-06-25 20:30:00\""); Date date = dateTime.toDate(); assertThat(mapper.writeValueAsString(date)).isEqualTo("\"19880625203000\""); } @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); assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class); } @Test public void noCustomPropertyNamingStrategy() throws Exception { this.context.register(JacksonAutoConfiguration.class); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); assertThat(mapper.getPropertyNamingStrategy()).isNull(); } @Test public void customPropertyNamingStrategyField() throws Exception { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.property-naming-strategy:SNAKE_CASE"); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); assertThat(mapper.getPropertyNamingStrategy()) .isInstanceOf(SnakeCaseStrategy.class); } @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.SnakeCaseStrategy"); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); assertThat(mapper.getPropertyNamingStrategy()) .isInstanceOf(SnakeCaseStrategy.class); } @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); assertThat(SerializationFeature.INDENT_OUTPUT.enabledByDefault()).isFalse(); assertThat(mapper.getSerializationConfig() .hasSerializationFeatures(SerializationFeature.INDENT_OUTPUT.getMask())) .isTrue(); } @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); assertThat(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.enabledByDefault()) .isTrue(); assertThat(mapper.getSerializationConfig().hasSerializationFeatures( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.getMask())).isFalse(); } @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); assertThat(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.enabledByDefault()) .isFalse(); assertThat(mapper.getDeserializationConfig().hasDeserializationFeatures( DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.getMask())).isTrue(); } @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); assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()) .isTrue(); assertThat(mapper.getDeserializationConfig().hasDeserializationFeatures( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.getMask())).isFalse(); } @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); assertThat(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.enabledByDefault()) .isFalse(); assertThat(mapper.getSerializationConfig() .hasMapperFeatures(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.getMask())) .isTrue(); assertThat(mapper.getDeserializationConfig() .hasMapperFeatures(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.getMask())) .isTrue(); } @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); assertThat(MapperFeature.USE_ANNOTATIONS.enabledByDefault()).isTrue(); assertThat(mapper.getDeserializationConfig() .hasMapperFeatures(MapperFeature.USE_ANNOTATIONS.getMask())).isFalse(); assertThat(mapper.getSerializationConfig() .hasMapperFeatures(MapperFeature.USE_ANNOTATIONS.getMask())).isFalse(); } @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); assertThat(JsonParser.Feature.ALLOW_SINGLE_QUOTES.enabledByDefault()).isFalse(); assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES)) .isTrue(); } @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); assertThat(JsonParser.Feature.AUTO_CLOSE_SOURCE.enabledByDefault()).isTrue(); assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)) .isFalse(); } @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); assertThat(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS.enabledByDefault()) .isFalse(); assertThat(mapper.getFactory() .isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)).isTrue(); } @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); assertThat(JsonGenerator.Feature.AUTO_CLOSE_TARGET.enabledByDefault()).isTrue(); assertThat(mapper.getFactory().isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET)) .isFalse(); } @Test public void defaultObjectMapperBuilder() throws Exception { this.context.register(JacksonAutoConfiguration.class); this.context.refresh(); Jackson2ObjectMapperBuilder builder = this.context .getBean(Jackson2ObjectMapperBuilder.class); ObjectMapper mapper = builder.build(); assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue(); assertThat(mapper.getDeserializationConfig() .isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse(); assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue(); assertThat(mapper.getDeserializationConfig() .isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse(); assertThat(mapper.getSerializationConfig() .isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse(); assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()) .isTrue(); assertThat(mapper.getDeserializationConfig() .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); } @Test public void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() { this.context.register(ModuleConfig.class, JacksonAutoConfiguration.class); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); assertThat(this.context.getBean(CustomModule.class).getOwners()) .contains((ObjectCodec) objectMapper); assertThat(objectMapper.canSerialize(LocalDateTime.class)).isTrue(); assertThat(objectMapper.canSerialize(Baz.class)).isTrue(); } @Test public void defaultSerializationInclusion() { this.context.register(JacksonAutoConfiguration.class); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion() .getValueInclusion()).isEqualTo(JsonInclude.Include.USE_DEFAULTS); } @Test public void customSerializationInclusion() { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.default-property-inclusion:non_null"); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion() .getValueInclusion()).isEqualTo(JsonInclude.Include.NON_NULL); } @Test public void customTimeZoneFormattingADateTime() throws JsonProcessingException { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.time-zone:America/Los_Angeles"); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.date-format:zzzz"); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.locale:en"); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); DateTime dateTime = new DateTime(1436966242231L, DateTimeZone.UTC); assertThat(objectMapper.writeValueAsString(dateTime)) .isEqualTo("\"Pacific Daylight Time\""); } @Test public void customTimeZoneFormattingADate() throws JsonProcessingException { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.time-zone:GMT+10"); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.date-format:z"); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); Date date = new Date(1436966242231L); assertThat(objectMapper.writeValueAsString(date)).isEqualTo("\"GMT+10:00\""); } @Test public void customLocale() throws JsonProcessingException { this.context.register(JacksonAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.locale:de"); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.date-format:zzzz"); this.context.refresh(); ObjectMapper objectMapper = this.context .getBean(Jackson2ObjectMapperBuilder.class).build(); DateTime dateTime = new DateTime(1436966242231L, DateTimeZone.UTC); assertThat(objectMapper.writeValueAsString(dateTime)) .isEqualTo("\"Koordinierte Universalzeit\""); } @Test public void additionalJacksonBuilderCustomization() throws Exception { this.context.register(JacksonAutoConfiguration.class, ObjectMapperBuilderCustomConfig.class); this.context.refresh(); ObjectMapper mapper = this.context.getBean(ObjectMapper.class); assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class); } @Test public void parameterNamesModuleIsAutoConfigured() { assertParameterNamesModuleCreatorBinding(Mode.DEFAULT, JacksonAutoConfiguration.class); } @Test public void customParameterNamesModuleCanBeConfigured() { assertParameterNamesModuleCreatorBinding(Mode.DELEGATING, ParameterNamesModuleConfig.class, JacksonAutoConfiguration.class); } private void assertParameterNamesModuleCreatorBinding(Mode expectedMode, Class<?>... configClasses) { this.context.register(configClasses); this.context.refresh(); DeserializationConfig deserializationConfig = this.context .getBean(ObjectMapper.class).getDeserializationConfig(); AnnotationIntrospector annotationIntrospector = deserializationConfig .getAnnotationIntrospector().allIntrospectors().iterator().next(); assertThat(ReflectionTestUtils.getField(annotationIntrospector, "creatorBinding")) .isEqualTo(expectedMode); } public static class MyDateFormat extends SimpleDateFormat { public MyDateFormat() { super("yyyy-MM-dd HH:mm:ss"); } } @Configuration protected static class MockObjectMapperConfig { @Bean @Primary public ObjectMapper objectMapper() { return mock(ObjectMapper.class); } } @Configuration @Import(BazSerializer.class) protected static class ModuleConfig { @Bean public CustomModule jacksonModule() { return new CustomModule(); } } @Configuration protected static class DoubleModulesConfig { @Bean public Module jacksonModule() { SimpleModule module = new SimpleModule(); module.addSerializer(Foo.class, new JsonSerializer<Foo>() { @Override public void serialize(Foo value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeStartObject(); jgen.writeStringField("foo", "bar"); jgen.writeEndObject(); } }); return module; } @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(jacksonModule()); return mapper; } } @Configuration protected static class ParameterNamesModuleConfig { @Bean public ParameterNamesModule parameterNamesModule() { return new ParameterNamesModule(JsonCreator.Mode.DELEGATING); } } @Configuration protected static class ObjectMapperBuilderCustomConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customDateFormat() { return new Jackson2ObjectMapperBuilderCustomizer() { @Override public void customize( Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) { jackson2ObjectMapperBuilder.dateFormat(new MyDateFormat()); } }; } } protected static final class Foo { private String name; private Foo() { } static Foo create() { return new Foo(); } public String getName() { return this.name; } public void setName(String name) { this.name = name; } } protected static class Bar { private String propertyName; public String getPropertyName() { return this.propertyName; } public void setPropertyName(String propertyName) { this.propertyName = propertyName; } } @JsonComponent private static class BazSerializer extends JsonObjectSerializer<Baz> { @Override protected void serializeObject(Baz value, JsonGenerator jgen, SerializerProvider provider) throws IOException { } } private static class Baz { } private static class CustomModule extends SimpleModule { private Set<ObjectCodec> owners = new HashSet<>(); @Override public void setupModule(SetupContext context) { this.owners.add(context.getOwner()); } Set<ObjectCodec> getOwners() { return this.owners; } } }