Description
Issue Description
When using Spring AI, I encountered a serialization failure for Java 8 date/time types (such as java.time.Duration
). Despite correctly including the jackson-datatype-jsr310 dependency in my project, the following error occurs at runtime:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.Duration` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: org.springframework.ai.chat.model.ChatResponse["metadata"]->org.springframework.ai.chat.metadata.ChatResponseMetadata["rateLimit"]->org.springframework.ai.chat.metadata.EmptyRateLimit["tokensReset"])
Root Cause
Through debugging, I discovered that the issue lies in the org.springframework.ai.util.JacksonUtils
class's instantiateAvailableModules() method. This method uses ClassUtils.forName()
to attempt loading the JavaTimeModule class, but fails due to class loader isolation issues.
The specific reasons are:
-
Spring Boot applications use multiple class loaders:
jdk.internal.loader.ClassLoaders$AppClassLoader
- JDK's default application class loaderorg.springframework.boot.loader.LaunchedURLClassLoader
- Spring Boot's class loader for application classes and dependencies
-
ClassUtils.getDefaultClassLoader()
returns the thread context class loader, which isAppClassLoader
, not theLaunchedURLClassLoader
that can see the application dependencies. -
Due to class loader visibility rules (parent class loaders cannot see classes loaded by child class loaders),
AppClassLoader
cannot see the JavaTimeModule class loaded byLaunchedURLClassLoader
. -
Verification tests:
Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule")
successfully loads the class because it uses the caller's class loader (LaunchedURLClassLoader
)ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null)
fails to load the class because it usesAppClassLoader
Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", false, ClassUtils.getDefaultClassLoader())
also fails to load the class
Steps to Reproduce
- Create a Spring Boot application with Spring AI dependencies
- Ensure jackson-datatype-jsr310 dependency is added
- Use Spring AI's
ChatClient
to send requests and handle responses - Attempt to serialize a
ChatResponse
object containingjava.time.Duration
fields
Temporary Workaround
Modify the ModelOptionsUtils.OBJECT_MAPPER
static field via reflection to directly register JavaTimeModule:
@Configuration
public class JacksonConfig {
@PostConstruct
public void fixModelOptionsUtilsObjectMapper() {
try {
// Get ModelOptionsUtils class
Class<?> modelOptionsUtilsClass = Class.forName("org.springframework.ai.model.ModelOptionsUtils");
// Get OBJECT_MAPPER field
Field objectMapperField = modelOptionsUtilsClass.getDeclaredField("OBJECT_MAPPER");
objectMapperField.setAccessible(true);
// Get current ObjectMapper instance
ObjectMapper currentMapper = (ObjectMapper) objectMapperField.get(null);
// Create JavaTimeModule instance directly, avoiding class loader issues
JavaTimeModule javaTimeModule = new JavaTimeModule();
// Register module
currentMapper.registerModule(javaTimeModule);
System.out.println("Successfully registered JavaTimeModule to ModelOptionsUtils.OBJECT_MAPPER");
} catch (Exception e) {
System.err.println("Failed to modify ModelOptionsUtils.OBJECT_MAPPER: " + e.getMessage());
e.printStackTrace();
}
}
}
Suggested Fix
I recommend modifying the JacksonUtils.instantiateAvailableModules()
method to use Class.forName()
instead of ClassUtils.forName()
, or explicitly specify the current class's class loader:
try {
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);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jsr310 not available
}
Or alternatively:
try {
ClassLoader currentClassLoader = JacksonUtils.class.getClassLoader();
Class<? extends com.fasterxml.jackson.databind.Module> javaTimeModuleClass =
(Class<? extends Module>) Class.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
true,
currentClassLoader);
com.fasterxml.jackson.databind.Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass);
modules.add(javaTimeModule);
}
catch (ClassNotFoundException ex) {
// jackson-datatype-jsr310 not available
}
Environment Information
- Spring AI version: 1.0.0-M7.MT2
- Spring Boot version: 2.7.18
- Java version: jdk17
- Jackson version: 2.18.3
Related Code
The issue appears in the following classes:
org.springframework.ai.util.JacksonUtils
- Responsible for loading Jackson modulesorg.springframework.ai.model.ModelOptionsUtils
- Contains the staticOBJECT_MAPPER
field- org.springframework.util.ClassUtils - Provides class loader utility methods
Additional Information
This issue is caused by a mismatch between Spring Boot's class loader hierarchy and Spring Framework's class loader usage patterns. In Spring Boot applications, application classes and dependencies are loaded by LaunchedURLClassLoader
, while Spring Framework's utility class ClassUtils defaults to using the thread context class loader (AppClassLoader
), resulting in an inability to find classes in application dependencies.