From eb5cb2d4e15ae76a42a00900cab12591a842c594 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Oct 2025 15:54:56 +0100 Subject: [PATCH] Consider Jackson 2 in HTTP codec auto-config See gh-47688 --- module/spring-boot-http-codec/build.gradle | 1 + .../CodecsAutoConfiguration.java | 53 +++++++++++++++++-- ...itional-spring-configuration-metadata.json | 30 +++++++++-- .../CodecsAutoConfigurationTests.java | 45 +++++++++++++++- 4 files changed, 121 insertions(+), 8 deletions(-) diff --git a/module/spring-boot-http-codec/build.gradle b/module/spring-boot-http-codec/build.gradle index 7eb8b70cec7..2fae3abe8ea 100644 --- a/module/spring-boot-http-codec/build.gradle +++ b/module/spring-boot-http-codec/build.gradle @@ -31,6 +31,7 @@ dependencies { optional(project(":core:spring-boot-autoconfigure")) optional(project(":core:spring-boot-test")) optional(project(":module:spring-boot-jackson")) + optional(project(":module:spring-boot-jackson2")) optional("org.springframework:spring-webflux") testImplementation(project(":core:spring-boot-test")) diff --git a/module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java b/module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java index f721ee63e96..550e3e79bdd 100644 --- a/module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java +++ b/module/spring-boot-http-codec/src/main/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfiguration.java @@ -16,18 +16,22 @@ package org.springframework.boot.http.codec.autoconfigure; +import com.fasterxml.jackson.databind.ObjectMapper; import org.jspecify.annotations.Nullable; -import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -43,14 +47,17 @@ import org.springframework.web.reactive.function.client.WebClient; * {@link org.springframework.core.codec.Decoder Decoders}. * * @author Brian Clozel - * @since 2.0.0 + * @since 4.0.0 */ -@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") +@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration", + "org.springframework.boot.jackson2.autoconfigure.Jackson2AutoConfiguration" }) @ConditionalOnClass({ CodecConfigurer.class, WebClient.class }) public final class CodecsAutoConfiguration { @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(ObjectMapper.class) + @ConditionalOnClass(JsonMapper.class) + @ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson", + matchIfMissing = true) static class JacksonJsonCodecConfiguration { @Bean @@ -66,6 +73,26 @@ public final class CodecsAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ObjectMapper.class) + @Conditional(NoJacksonOrJackson2Preferred.class) + @Deprecated(since = "4.0.0", forRemoval = true) + @SuppressWarnings("removal") + static class Jackson2JsonCodecConfiguration { + + @Bean + @Order(0) + @ConditionalOnBean(ObjectMapper.class) + CodecCustomizer jackson2CodecCustomizer(ObjectMapper objectMapper) { + return (configurer) -> { + CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs(); + defaults.jacksonJsonDecoder(new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper)); + defaults.jacksonJsonEncoder(new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper)); + }; + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(HttpCodecsProperties.class) static class DefaultCodecsConfiguration { @@ -104,4 +131,22 @@ public final class CodecsAutoConfiguration { } + static class NoJacksonOrJackson2Preferred extends AnyNestedCondition { + + NoJacksonOrJackson2Preferred() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass("tools.jackson.databind.json.JsonMapper") + static class NoJackson { + + } + + @ConditionalOnProperty(name = "spring.http.codecs.preferred-json-mapper", havingValue = "jackson2") + static class Jackson2Preferred { + + } + + } + } diff --git a/module/spring-boot-http-codec/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-http-codec/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 671fd2520c2..61e148b885c 100644 --- a/module/spring-boot-http-codec/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-http-codec/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,4 +1,28 @@ -{ - "groups": [], - "properties": [] +{ + "properties": [ + { + "name": "spring.http.codecs.preferred-json-mapper", + "type": "java.lang.String", + "defaultValue": "jackson", + "description": "Preferred JSON mapper to use for HTTP encoding and decoding. By default, auto-detected according to the environment. Supported values are 'jackson' and 'jackson2' (deprecated)." + } + ], + "hints": [ + { + "name": "spring.http.codecs.preferred-json-mapper", + "values": [ + { + "value": "jackson" + }, + { + "value": "jackson2" + } + ], + "providers": [ + { + "name": "any" + } + ] + } + ] } diff --git a/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java b/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java index 39e7d050b67..978b3ec400a 100644 --- a/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java +++ b/module/spring-boot-http-codec/src/test/java/org/springframework/boot/http/codec/autoconfigure/CodecsAutoConfigurationTests.java @@ -18,11 +18,13 @@ package org.springframework.boot.http.codec.autoconfigure; import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import tools.jackson.databind.json.JsonMapper; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.http.codec.CodecCustomizer; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -72,7 +74,7 @@ class CodecsAutoConfigurationTests { } @Test - void jacksonCodecCustomizerBacksOffWhenThereIsNoObjectMapper() { + void jacksonCodecCustomizerBacksOffWhenThereIsNoJsonMapper() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("jacksonCodecCustomizer")); } @@ -82,6 +84,37 @@ class CodecsAutoConfigurationTests { .run((context) -> assertThat(context).hasBean("jacksonCodecCustomizer")); } + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + void jacksonCodecCustomizerBacksOffWhenJackson2IsPreferred() { + this.contextRunner.withUserConfiguration(JsonMapperConfiguration.class) + .withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2") + .run((context) -> assertThat(context).doesNotHaveBean("jacksonCodecCustomizer")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + void jackson2CodecCustomizerIsAutoConfiguredWhenObjectMapperIsPresentAndJackson2IsPreferred() { + this.contextRunner.withUserConfiguration(ObjectMapperConfiguration.class) + .withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2") + .run((context) -> assertThat(context).hasBean("jackson2CodecCustomizer")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + void jackson2CodecCustomizerIsAutoConfiguredWhenObjectMapperIsPresentAndJacksonIsMissing() { + this.contextRunner.withUserConfiguration(ObjectMapperConfiguration.class) + .withClassLoader(new FilteredClassLoader(JsonMapper.class.getPackage().getName())) + .run((context) -> assertThat(context).hasBean("jackson2CodecCustomizer")); + } + + @Test + @Deprecated(since = "4.0.0", forRemoval = true) + void jackson2CodecCustomizerBacksOffWhenJackson2IsPreferredButThereIsNoObjectMapper() { + this.contextRunner.withPropertyValues("spring.http.codecs.preferred-json-mapper=jackson2") + .run((context) -> assertThat(context).doesNotHaveBean("jackson2CodecCustomizer")); + } + @Test void userProvidedCustomizerCanOverrideJacksonCodecCustomizer() { this.contextRunner.withUserConfiguration(JsonMapperConfiguration.class, CodecCustomizerConfiguration.class) @@ -116,6 +149,16 @@ class CodecsAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class ObjectMapperConfiguration { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + } + @Configuration(proxyBeanMethods = false) static class CodecCustomizerConfiguration {