From 04223058f1dcc3fc523319444643bc80b71a68b8 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 19 Mar 2019 16:58:14 +0100 Subject: [PATCH] Fix Jackson builder modulesToInstall override behavior This commit updates Jackson2ObjectMapperBuilder in order to ensure that modules specified via modulesToInstall eventually override the default ones. Closes gh-22624 --- build.gradle | 1 + .../json/Jackson2ObjectMapperBuilder.java | 48 ++++++++----- .../Jackson2ObjectMapperBuilderTests.java | 68 ++++++++++++++++++- 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index f3453b7fb7a..1b905676460 100644 --- a/build.gradle +++ b/build.gradle @@ -727,6 +727,7 @@ project("spring-web") { testCompile("com.fasterxml.jackson.datatype:jackson-datatype-joda:2.8.11") testCompile("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.8.11") testCompile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.11.1") + testCompile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.11") testCompile("com.squareup.okhttp3:mockwebserver:${okhttp3Version}") testRuntime("com.sun.mail:javax.mail:${javamailVersion}") } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index 046a3b1e9a8..54ac4c0e972 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2019 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. @@ -599,25 +599,32 @@ public class Jackson2ObjectMapperBuilder { public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); + Map modulesToRegister = new LinkedHashMap(); if (this.findModulesViaServiceLoader) { // Jackson 2.2+ - objectMapper.registerModules(ObjectMapper.findModules(this.moduleClassLoader)); + for (Module module : ObjectMapper.findModules(this.moduleClassLoader)) { + modulesToRegister.put(module.getTypeId(), module); + } } else if (this.findWellKnownModules) { - registerWellKnownModulesIfAvailable(objectMapper); + registerWellKnownModulesIfAvailable(modulesToRegister); } if (this.modules != null) { for (Module module : this.modules) { - // Using Jackson 2.0+ registerModule method, not Jackson 2.2+ registerModules - objectMapper.registerModule(module); + modulesToRegister.put(module.getTypeId(), module); } } if (this.moduleClasses != null) { - for (Class module : this.moduleClasses) { - objectMapper.registerModule(BeanUtils.instantiate(module)); + for (Class moduleClass : this.moduleClasses) { + Module module = BeanUtils.instantiateClass(moduleClass); + modulesToRegister.put(module.getTypeId(), module); } } + // Using Jackson 2.0+ registerModule method, not Jackson 2.2+ registerModules + for (Module module : modulesToRegister.values()) { + objectMapper.registerModule(module); + } if (this.dateFormat != null) { objectMapper.setDateFormat(this.dateFormat); @@ -719,13 +726,14 @@ public class Jackson2ObjectMapperBuilder { } @SuppressWarnings("unchecked") - private void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) { + private void registerWellKnownModulesIfAvailable(Map modulesToRegister) { // Java 7 java.nio.file.Path class present? if (ClassUtils.isPresent("java.nio.file.Path", this.moduleClassLoader)) { try { - Class jdk7Module = (Class) + Class jdk7ModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk7.Jdk7Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(jdk7Module)); + Module jdk7Module = BeanUtils.instantiateClass(jdk7ModuleClass); + modulesToRegister.put(jdk7Module.getTypeId(), jdk7Module); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk7 not available @@ -735,9 +743,10 @@ public class Jackson2ObjectMapperBuilder { // Java 8 java.util.Optional class present? if (ClassUtils.isPresent("java.util.Optional", this.moduleClassLoader)) { try { - Class jdk8Module = (Class) + Class jdk8ModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module)); + Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass); + modulesToRegister.put(jdk8Module.getTypeId(), jdk8Module); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk8 not available @@ -747,9 +756,10 @@ public class Jackson2ObjectMapperBuilder { // Java 8 java.time package present? if (ClassUtils.isPresent("java.time.LocalDate", this.moduleClassLoader)) { try { - Class javaTimeModule = (Class) + Class javaTimeModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule)); + Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); + modulesToRegister.put(javaTimeModule.getTypeId(), javaTimeModule); } catch (ClassNotFoundException ex) { // jackson-datatype-jsr310 not available @@ -759,9 +769,10 @@ public class Jackson2ObjectMapperBuilder { // Joda-Time present? if (ClassUtils.isPresent("org.joda.time.LocalDate", this.moduleClassLoader)) { try { - Class jodaModule = (Class) + Class jodaModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule)); + Module jodaModule = BeanUtils.instantiateClass(jodaModuleClass); + modulesToRegister.put(jodaModule.getTypeId(), jodaModule); } catch (ClassNotFoundException ex) { // jackson-datatype-joda not available @@ -771,9 +782,10 @@ public class Jackson2ObjectMapperBuilder { // Kotlin present? if (ClassUtils.isPresent("kotlin.Unit", this.moduleClassLoader)) { try { - Class kotlinModule = (Class) + Class kotlinModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); - objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule)); + Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); + modulesToRegister.put(kotlinModule.getTypeId(), kotlinModule); } catch (ClassNotFoundException ex) { // jackson-module-kotlin not available diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index ddb9e1e6ebe..30460e17421 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2019 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. @@ -21,6 +21,8 @@ import java.io.UnsupportedEncodingException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,6 +40,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; @@ -48,6 +51,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.cfg.DeserializerFactoryConfig; import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; import com.fasterxml.jackson.databind.deser.BasicDeserializerFactory; @@ -64,12 +68,14 @@ import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializer; import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import kotlin.ranges.IntRange; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Test; import org.springframework.beans.FatalBeanException; +import org.springframework.util.StringUtils; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -84,6 +90,8 @@ public class Jackson2ObjectMapperBuilderTests { private static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final String DATA = "{\"offsetDateTime\": \"2020-01-01T00:00:00\"}"; + @Test public void settersWithNullValues() { @@ -289,6 +297,18 @@ public class Jackson2ObjectMapperBuilderTests { assertThat(new String(objectMapper.writeValueAsBytes(new Integer(4)), "UTF-8"), containsString("customid")); } + @Test // gh-22576 + public void overrideWellKnownModuleWithModule() throws IOException { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer()); + builder.modulesToInstall(javaTimeModule); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + ObjectMapper objectMapper = builder.build(); + DemoPojo demoPojo = objectMapper.readValue(DATA, DemoPojo.class); + assertNotNull(demoPojo.getOffsetDateTime()); + } + private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); @@ -540,4 +560,50 @@ public class Jackson2ObjectMapperBuilderTests { } } + public static class JacksonVisibilityBean { + + private String property1; + + public String property2; + + public String getProperty3() { + return null; + } + + } + + static class OffsetDateTimeDeserializer extends JsonDeserializer { + + private static final String CURRENT_ZONE_OFFSET = OffsetDateTime.now().getOffset().toString(); + + @Override + public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + final String value = jsonParser.getValueAsString(); + if (StringUtils.isEmpty(value)) { + return null; + } + try { + return OffsetDateTime.parse(value); + + } catch (DateTimeParseException exception) { + return OffsetDateTime.parse(value + CURRENT_ZONE_OFFSET); + } + } + } + + @JsonDeserialize + static class DemoPojo { + + private OffsetDateTime offsetDateTime; + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + } + }