From 6e4b88d85048100a62ec2938b91fe3fad68091d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Oct 2025 09:08:26 +0100 Subject: [PATCH] Add property to configure Jackson 3 with Boot's Jackson 2 defaults Closes gh- --- .../modules/how-to/pages/spring-mvc.adoc | 2 + .../JacksonAutoConfiguration.java | 6 +++ .../autoconfigure/JacksonProperties.java | 14 +++++ .../JacksonAutoConfigurationTests.java | 52 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 1566ce3b395..ed9b567a752 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -104,6 +104,8 @@ Note that, thanks to the use of xref:reference:features/external-config.adoc#fea This environment-based configuration is applied to the auto-configured javadoc:tools.jackson.databind.json.JsonMapper.Builder[] bean and applies to any mappers created by using the builder, including the auto-configured javadoc:tools.jackson.databind.json.JsonMapper[] bean. +To ease the migration when working on an application that previously used Jackson 2, the auto-configured `JsonMapper` can be configured to use defaults that are as close as possible to those that Spring Boot used for Jackson 2. To enable these defaults, set configprop:spring.jackson.use-jackson2-defaults[] to `true`. + The context's javadoc:tools.jackson.databind.json.JsonMapper.Builder[] can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer[] beans. Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization. diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java index 061ef637dd6..71d2bb96596 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfiguration.java @@ -36,6 +36,7 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.PropertyNamingStrategies; import tools.jackson.databind.PropertyNamingStrategy; import tools.jackson.databind.cfg.ConstructorDetector; +import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.json.JsonMapper; import org.springframework.aot.hint.ReflectionHints; @@ -164,6 +165,11 @@ public final class JacksonAutoConfiguration { @Override public void customize(JsonMapper.Builder builder) { + if (this.jacksonProperties.isUseJackson2Defaults()) { + builder.configureForJackson2() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, + DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS); + } if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { builder.changeDefaultPropertyInclusion((handler) -> handler .withValueInclusion(this.jacksonProperties.getDefaultPropertyInclusion())); diff --git a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java index 13cde58244d..d77f3ba0354 100644 --- a/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java +++ b/module/spring-boot-jackson/src/main/java/org/springframework/boot/jackson/autoconfigure/JacksonProperties.java @@ -109,6 +109,12 @@ public class JacksonProperties { */ private @Nullable Locale locale; + /** + * Whether to configure Jackson 3 with the same defaults as Spring Boot previously + * used for Jackson 2. + */ + private boolean useJackson2Defaults = false; + private final Datatype datatype = new Datatype(); private final Json json = new Json(); @@ -185,6 +191,14 @@ public class JacksonProperties { this.locale = locale; } + public boolean isUseJackson2Defaults() { + return this.useJackson2Defaults; + } + + public void setUseJackson2Defaults(boolean useJackson2Defaults) { + this.useJackson2Defaults = useJackson2Defaults; + } + public Datatype getDatatype() { return this.datatype; } diff --git a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java index e49ee40778a..41451bdd9ea 100644 --- a/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java +++ b/module/spring-boot-jackson/src/test/java/org/springframework/boot/jackson/autoconfigure/JacksonAutoConfigurationTests.java @@ -29,6 +29,8 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import tools.jackson.core.JsonGenerator; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.StreamWriteFeature; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.core.json.JsonWriteFeature; import tools.jackson.databind.DeserializationFeature; @@ -539,6 +541,56 @@ class JacksonAutoConfigurationTests { }); } + @Test + void whenUsingJackson2DefaultsShouldBeConfiguredUsingConfigureForJackson2() { + this.contextRunner.withPropertyValues("spring.jackson.use-jackson2-defaults=true").run((context) -> { + JsonMapper jsonMapper = context.getBean(JsonMapper.class); + JsonMapper jackson2ConfiguredJsonMapper = JsonMapper.builder().configureForJackson2().build(); + for (DateTimeFeature feature : DateTimeFeature.values()) { + boolean expected = (feature == DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS + || feature == DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS) ? false + : jackson2ConfiguredJsonMapper.isEnabled(feature); + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()).isEqualTo(expected); + } + for (DeserializationFeature feature : DeserializationFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (EnumFeature feature : EnumFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (JsonNodeFeature feature : JsonNodeFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (JsonReadFeature feature : JsonReadFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (JsonWriteFeature feature : JsonWriteFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (MapperFeature feature : MapperFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (SerializationFeature feature : SerializationFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (StreamReadFeature feature : StreamReadFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + for (StreamWriteFeature feature : StreamWriteFeature.values()) { + assertThat(jsonMapper.isEnabled(feature)).as(feature.name()) + .isEqualTo(jackson2ConfiguredJsonMapper.isEnabled(feature)); + } + }); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() {