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 ed9b567a752..084eea90aaf 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 @@ -110,7 +110,9 @@ The context's javadoc:tools.jackson.databind.json.JsonMapper.Builder[] can be cu 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. Any beans of type javadoc:tools.jackson.databind.JacksonModule[] are automatically registered with the auto-configured javadoc:tools.jackson.databind.json.JsonMapper.Builder[] and are applied to any javadoc:tools.jackson.databind.json.JsonMapper[] instances that it creates. -This provides a global mechanism for contributing custom modules when you add new features to your application. +This provides an application-wide mechanism for contributing custom modules when you add new features to your application. + +Any modules that participate in the Java `ServiceLoader` mechanism are, by default, found and added to the auto-configured javadoc:tools.jackson.databind.json.JsonMapper.Builder[]. To disable this behavior, set configprop:spring.jackson.find-and-add-modules[] to `false`. If you want to replace the default javadoc:tools.jackson.databind.json.JsonMapper[] completely, either define a javadoc:org.springframework.context.annotation.Bean[format=annotation] of that type or, if you prefer the builder-based approach, define a javadoc:tools.jackson.databind.json.JsonMapper.Builder[] javadoc:org.springframework.context.annotation.Bean[format=annotation]. When defining an javadoc:tools.jackson.databind.json.JsonMapper[] bean, marking it as javadoc:org.springframework.context.annotation.Primary[format=annotation] is recommended as the auto-configuration's javadoc:tools.jackson.databind.json.JsonMapper[] that it will replace is javadoc:org.springframework.context.annotation.Primary[format=annotation]. diff --git a/module/spring-boot-jackson/build.gradle b/module/spring-boot-jackson/build.gradle index 10c6fafdba8..bc36c4383c2 100644 --- a/module/spring-boot-jackson/build.gradle +++ b/module/spring-boot-jackson/build.gradle @@ -37,6 +37,7 @@ dependencies { testImplementation(project(":core:spring-boot-test")) testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("tools.jackson.module:jackson-module-kotlin") testRuntimeOnly("ch.qos.logback:logback-classic") } 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 71d2bb96596..74900fd0089 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 @@ -170,6 +170,9 @@ public final class JacksonAutoConfiguration { .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, DateTimeFeature.WRITE_DURATIONS_AS_TIMESTAMPS); } + if (this.jacksonProperties.isFindAndAddModules()) { + builder.findAndAddModules(getClass().getClassLoader()); + } 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 d77f3ba0354..6be7ee646c7 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 @@ -115,6 +115,12 @@ public class JacksonProperties { */ private boolean useJackson2Defaults = false; + /** + * Whether to find and add modules to the auto-configured JsonMapper.Builder using + * MapperBuilder.findAndAddModules(ClassLoader). + */ + private boolean findAndAddModules = true; + private final Datatype datatype = new Datatype(); private final Json json = new Json(); @@ -199,6 +205,14 @@ public class JacksonProperties { this.useJackson2Defaults = useJackson2Defaults; } + public boolean isFindAndAddModules() { + return this.findAndAddModules; + } + + public void setFindAndAddModules(boolean findModules) { + this.findAndAddModules = findModules; + } + 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 41451bdd9ea..cc3edcf9fbc 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 @@ -52,6 +52,7 @@ import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.json.JsonMapper.Builder; import tools.jackson.databind.module.SimpleModule; import tools.jackson.databind.util.StdDateFormat; +import tools.jackson.module.kotlin.KotlinModule; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -269,11 +270,11 @@ class JacksonAutoConfigurationTests { @Test void disableMapperFeature() { - this.contextRunner.withPropertyValues("spring.jackson.mapper.use_annotations:false").run((context) -> { + this.contextRunner.withPropertyValues("spring.jackson.mapper.infer-property-mutators:false").run((context) -> { JsonMapper mapper = context.getBean(JsonMapper.class); - assertThat(MapperFeature.USE_ANNOTATIONS.enabledByDefault()).isTrue(); - assertThat(mapper.deserializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse(); - assertThat(mapper.serializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse(); + assertThat(MapperFeature.INFER_PROPERTY_MUTATORS.enabledByDefault()).isTrue(); + assertThat(mapper.deserializationConfig().isEnabled(MapperFeature.INFER_PROPERTY_MUTATORS)).isFalse(); + assertThat(mapper.serializationConfig().isEnabled(MapperFeature.INFER_PROPERTY_MUTATORS)).isFalse(); }); } @@ -591,6 +592,19 @@ class JacksonAutoConfigurationTests { }); } + @Test + void shouldFindAndAddModulesByDefault() { + this.contextRunner.run((context) -> assertThat(context.getBean(JsonMapper.class).getRegisteredModules()) + .hasAtLeastOneElementOfType(KotlinModule.class)); + } + + @Test + void shouldNotFindAndAddModulesWhenDisabled() { + this.contextRunner.withPropertyValues("spring.jackson.find-and-add-modules=false") + .run((context) -> assertThat(context.getBean(JsonMapper.class).getRegisteredModules()) + .doesNotHaveAnyElementsOfTypes(KotlinModule.class)); + } + static class MyDateFormat extends SimpleDateFormat { MyDateFormat() {