diff --git a/spring-ai-commons/pom.xml b/spring-ai-commons/pom.xml
index 513877df8ed..97b9b461d49 100644
--- a/spring-ai-commons/pom.xml
+++ b/spring-ai-commons/pom.xml
@@ -106,6 +106,11 @@
jackson-module-kotlin
test
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ test
+
diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java b/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java
index 3686dd417ca..41e2129bd73 100644
--- a/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java
+++ b/spring-ai-commons/src/main/java/org/springframework/ai/util/JacksonUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023-2024 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -23,7 +23,6 @@
import org.springframework.beans.BeanUtils;
import org.springframework.core.KotlinDetector;
-import org.springframework.util.ClassUtils;
/**
* Utility methods for Jackson.
@@ -43,8 +42,8 @@ public abstract class JacksonUtils {
public static List instantiateAvailableModules() {
List modules = new ArrayList<>();
try {
- Class extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class extends Module>) ClassUtils
- .forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null);
+ Class extends com.fasterxml.jackson.databind.Module> jdk8ModuleClass = (Class extends Module>) Class
+ .forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module");
com.fasterxml.jackson.databind.Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass);
modules.add(jdk8Module);
}
@@ -53,8 +52,8 @@ public static List instantiateAvailableModules() {
}
try {
- Class extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class extends Module>) ClassUtils
- .forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null);
+ Class extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass = (Class extends Module>) Class
+ .forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule");
com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
modules.add(javaTimeModule);
}
@@ -63,8 +62,8 @@ public static List instantiateAvailableModules() {
}
try {
- Class extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class extends Module>) ClassUtils
- .forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null);
+ Class extends com.fasterxml.jackson.databind.Module> parameterNamesModuleClass = (Class extends Module>) Class
+ .forName("com.fasterxml.jackson.module.paramnames.ParameterNamesModule");
com.fasterxml.jackson.databind.Module parameterNamesModule = BeanUtils
.instantiateClass(parameterNamesModuleClass);
modules.add(parameterNamesModule);
@@ -76,8 +75,8 @@ public static List instantiateAvailableModules() {
// Kotlin present?
if (KotlinDetector.isKotlinPresent()) {
try {
- Class extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class extends Module>) ClassUtils
- .forName("com.fasterxml.jackson.module.kotlin.KotlinModule", null);
+ Class extends com.fasterxml.jackson.databind.Module> kotlinModuleClass = (Class extends Module>) Class
+ .forName("com.fasterxml.jackson.module.kotlin.KotlinModule");
Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass);
modules.add(kotlinModule);
}
diff --git a/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java b/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java
new file mode 100644
index 00000000000..582a0a4b577
--- /dev/null
+++ b/spring-ai-commons/src/test/java/org/springframework/ai/util/JacksonUtilsTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2025-2025 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.ai.util;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class JacksonUtilsTests {
+
+ /*
+ * Make sure that JacksonUtils use the correct classloader to load modules. See
+ * https://github.com/spring-projects/spring-ai/issues/2921
+ */
+ @Test
+ void usesCorrectClassLoader() throws JsonProcessingException, ClassNotFoundException {
+ ClassLoader previousLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ // This parent CL cannot see the clazz class below. But this shouldn't matter.
+ Thread.currentThread().setContextClassLoader(getClass().getClassLoader().getParent());
+ // Should work whatever the current Thread context CL is
+ var jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();
+ Class> clazz = getClass().getClassLoader().loadClass(getClass().getName() + "$Cell");
+ var output = jsonMapper.readValue("{\"name\":\"Amoeba\",\"lifespan\":\"PT42S\"}", clazz);
+ assertThat(output).isEqualTo(new Cell("Amoeba", Duration.of(42L, ChronoUnit.SECONDS)));
+
+ }
+ finally {
+ Thread.currentThread().setContextClassLoader(previousLoader);
+ }
+
+ }
+
+ record Cell(String name, Duration lifespan) {
+ }
+
+}