From 7b60227f4156562bc0ae3347050bab63d6b8f20b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Oct 2025 15:32:01 +0100 Subject: [PATCH] Consider Jackson 2 in HTTP message converter auto-config See gh-47688 --- .../spring-boot-http-converter/build.gradle | 2 + ...sonHttpMessageConvertersConfiguration.java | 6 + ...ttpMessageConvertersAutoConfiguration.java | 7 +- ...on2HttpMessageConvertersConfiguration.java | 93 ++++++++++++++++ ...onbHttpMessageConvertersConfiguration.java | 5 +- ...itional-spring-configuration-metadata.json | 5 +- ...ssageConvertersAutoConfigurationTests.java | 103 +++++++++++++++++- 7 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java diff --git a/module/spring-boot-http-converter/build.gradle b/module/spring-boot-http-converter/build.gradle index 9bb40cb0869..1c85d7d28a9 100644 --- a/module/spring-boot-http-converter/build.gradle +++ b/module/spring-boot-http-converter/build.gradle @@ -33,8 +33,10 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":module:spring-boot-gson")) optional(project(":module:spring-boot-jackson")) + optional(project(":module:spring-boot-jackson2")) optional(project(":module:spring-boot-jsonb")) optional(project(":module:spring-boot-kotlin-serialization")) + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") optional("com.google.code.gson:gson") optional("jakarta.json.bind:jakarta.json.bind-api") optional("org.springframework:spring-webmvc") diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java index 17528f4c61e..81d6d7cdb44 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/GsonHttpMessageConvertersConfiguration.java @@ -85,6 +85,12 @@ class GsonHttpMessageConvertersConfiguration { } + @SuppressWarnings("removal") + @ConditionalOnBean(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class) + static class Jackson2Available { + + } + @ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY, havingValue = "jsonb") static class JsonbPreferred { diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java index 324320cd9a7..bace77bb3f6 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfiguration.java @@ -51,14 +51,17 @@ import org.springframework.http.converter.StringHttpMessageConverter; * @author Brian Clozel * @since 4.0.0 */ +@SuppressWarnings("removal") @AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration", + "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration", "org.springframework.boot.jsonb.autoconfigure.JsonbAutoConfiguration", "org.springframework.boot.gson.autoconfigure.GsonAutoConfiguration", "org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration" }) @ConditionalOnClass(HttpMessageConverter.class) @Conditional(NotReactiveWebApplicationCondition.class) -@Import({ JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class, - JsonbHttpMessageConvertersConfiguration.class, KotlinSerializationHttpMessageConvertersConfiguration.class }) +@Import({ JacksonHttpMessageConvertersConfiguration.class, Jackson2HttpMessageConvertersConfiguration.class, + GsonHttpMessageConvertersConfiguration.class, JsonbHttpMessageConvertersConfiguration.class, + KotlinSerializationHttpMessageConvertersConfiguration.class }) public final class HttpMessageConvertersAutoConfiguration { static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java new file mode 100644 index 00000000000..7164f80bf26 --- /dev/null +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/Jackson2HttpMessageConvertersConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-present 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 + * + * https://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.http.converter.autoconfigure; + +import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.xml.XmlMapper; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; + +/** + * Configuration for HTTP message converters that use Jackson 2. + * + * @author Andy Wilkinson + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3. + */ +@Configuration(proxyBeanMethods = false) +@Deprecated(since = "4.0.0", forRemoval = true) +@SuppressWarnings({ "deprecation", "removal" }) +class Jackson2HttpMessageConvertersConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ObjectMapper.class) + @ConditionalOnBean(ObjectMapper.class) + @Conditional(PreferJackson2OrJacksonUnavailableCondition.class) + static class MappingJackson2HttpMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( + ObjectMapper objectMapper) { + return new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(objectMapper); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(XmlMapper.class) + @ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class) + protected static class MappingJackson2XmlHttpMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter( + Jackson2ObjectMapperBuilder builder) { + return new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()); + } + + } + + private static class PreferJackson2OrJacksonUnavailableCondition extends AnyNestedCondition { + + PreferJackson2OrJacksonUnavailableCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY, + havingValue = "jackson2") + static class Jackson2Preferred { + + } + + @ConditionalOnMissingBean(JacksonJsonHttpMessageConverter.class) + static class JacksonUnavailable { + + } + + } + +} diff --git a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java index 7669010ae60..be2616b0e7e 100644 --- a/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java +++ b/module/spring-boot-http-converter/src/main/java/org/springframework/boot/http/converter/autoconfigure/JsonbHttpMessageConvertersConfiguration.java @@ -66,7 +66,10 @@ class JsonbHttpMessageConvertersConfiguration { } - @ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, GsonHttpMessageConverter.class }) + @SuppressWarnings("removal") + @ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, + GsonHttpMessageConverter.class }) static class JacksonAndGsonMissing { } diff --git a/module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d6d92477e76..dfdbb97f5bb 100644 --- a/module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-http-converter/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -4,7 +4,7 @@ "name": "spring.http.converters.preferred-json-mapper", "type": "java.lang.String", "defaultValue": "jackson", - "description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'gson', 'jsonb' and 'kotlin-serialization'. When other json mapping libraries are present, use a custom HttpMessageConverters bean to control the preferred mapper." + "description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'jackson2' (deprecated), 'gson', 'jsonb' and 'kotlin-serialization'. When other json mapping libraries are present, use a custom HttpMessageConverters bean to control the preferred mapper." } ], "hints": [ @@ -17,6 +17,9 @@ { "value": "jackson" }, + { + "value": "jackson2" + }, { "value": "jsonb" } diff --git a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java index 2635e75a730..e636dc87b8d 100644 --- a/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java +++ b/module/spring-boot-http-converter/src/test/java/org/springframework/boot/http/converter/autoconfigure/HttpMessageConvertersAutoConfigurationTests.java @@ -19,18 +19,20 @@ package org.springframework.boot.http.converter.autoconfigure; import java.nio.charset.StandardCharsets; import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import kotlinx.serialization.json.Json; import org.junit.jupiter.api.Test; -import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.dataformat.xml.XmlMapper; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.http.converter.autoconfigure.JacksonHttpMessageConvertersConfiguration.JacksonJsonHttpMessageConverterConfiguration; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -77,7 +79,7 @@ class HttpMessageConvertersAutoConfigurationTests { @Test void jacksonNotAvailable() { this.contextRunner.run((context) -> { - assertThat(context).doesNotHaveBean(ObjectMapper.class); + assertThat(context).doesNotHaveBean(JsonMapper.class); assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class); assertThat(context).doesNotHaveBean(JacksonXmlHttpMessageConverter.class); }); @@ -107,6 +109,34 @@ class HttpMessageConvertersAutoConfigurationTests { .run(assertConverter(JacksonJsonHttpMessageConverter.class, "customJacksonMessageConverter")); } + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void jackson2DefaultConverter() { + this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, + "mappingJackson2HttpMessageConverter")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void jackson2ConverterWithBuilder() { + this.contextRunner.withUserConfiguration(Jackson2ObjectMapperBuilderConfig.class) + .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, + "mappingJackson2HttpMessageConverter")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + void jackson2CustomConverter() { + this.contextRunner.withUserConfiguration(Jackson2ObjectMapperConfig.class, Jackson2ConverterConfig.class) + .run(assertConverter(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, + "customJacksonMessageConverter")); + } + @Test void gsonNotAvailable() { this.contextRunner.run((context) -> { @@ -244,8 +274,26 @@ class HttpMessageConvertersAutoConfigurationTests { } @Test - void gsonIsPreferredIfJacksonIsNotAvailable() { - allOptionsRunner().withClassLoader(new FilteredClassLoader(ObjectMapper.class.getPackage().getName())) + @SuppressWarnings("removal") + void jackson2IsPreferredIfJacksonIsNotAvailable() { + allOptionsRunner().withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName())) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run((context) -> { + assertConverterBeanExists(context, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class, + "mappingJackson2HttpMessageConverter"); + assertConverterBeanRegisteredWithHttpMessageConverters(context, + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.class); + assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); + assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); + }); + } + + @Test + void gsonIsPreferredIfJacksonAndJackson2AreNotAvailable() { + allOptionsRunner() + .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), + ObjectMapper.class.getPackage().getName())) .run((context) -> { assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); @@ -256,8 +304,8 @@ class HttpMessageConvertersAutoConfigurationTests { @Test void jsonbIsPreferredIfJacksonAndGsonAreNotAvailable() { allOptionsRunner() - .withClassLoader(new FilteredClassLoader(ObjectMapper.class.getPackage().getName(), - Gson.class.getPackage().getName())) + .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName(), + ObjectMapper.class.getPackage().getName(), Gson.class.getPackage().getName())) .run(assertConverter(JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter")); } @@ -303,6 +351,7 @@ class HttpMessageConvertersAutoConfigurationTests { private ApplicationContextRunner allOptionsRunner() { return this.contextRunner.withBean(Gson.class) .withBean(JsonMapper.class) + .withBean(ObjectMapper.class, ObjectMapper::new) .withBean(Jsonb.class, JsonbBuilder::create) .withBean(Json.class, () -> Json.Default); } @@ -403,6 +452,48 @@ class HttpMessageConvertersAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class Jackson2ObjectMapperConfig { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + } + + @Configuration(proxyBeanMethods = false) + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + static class Jackson2ObjectMapperBuilderConfig { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder() { + return new org.springframework.http.converter.json.Jackson2ObjectMapperBuilder(); + } + + } + + @Deprecated(since = "4.0.0", forRemoval = true) + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class Jackson2ConverterConfig { + + @Bean + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter customJacksonMessageConverter( + ObjectMapper objectMapper) { + org.springframework.http.converter.json.MappingJackson2HttpMessageConverter converter = new org.springframework.http.converter.json.MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(objectMapper); + return converter; + } + + } + @Configuration(proxyBeanMethods = false) static class GsonConverterConfig {