Skip to content

Commit 9550bd1

Browse files
committed
Allow using spring's object mapper
Signed-off-by: Daniel Albuquerque <daniel.albuquerque@teya.com>
1 parent 920e6f4 commit 9550bd1

File tree

10 files changed

+456
-36
lines changed

10 files changed

+456
-36
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai-parent</artifactId>
9+
<version>2.0.0-SNAPSHOT</version>
10+
<relativePath>../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-jackson</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI Model Auto Configuration</name>
15+
<description>Spring AI Model Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
25+
<dependencies>
26+
27+
<dependency>
28+
<groupId>org.springframework.ai</groupId>
29+
<artifactId>spring-ai-model</artifactId>
30+
<version>${project.parent.version}</version>
31+
</dependency>
32+
33+
<!-- Boot dependencies -->
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter</artifactId>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-configuration-processor</artifactId>
42+
<optional>true</optional>
43+
</dependency>
44+
45+
<dependency>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
48+
<optional>true</optional>
49+
</dependency>
50+
51+
<!-- Test dependencies -->
52+
<dependency>
53+
<groupId>org.springframework.ai</groupId>
54+
<artifactId>spring-ai-test</artifactId>
55+
<version>${project.parent.version}</version>
56+
<scope>test</scope>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>org.springframework.boot</groupId>
61+
<artifactId>spring-boot-starter-test</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
65+
<dependency>
66+
<groupId>org.mockito</groupId>
67+
<artifactId>mockito-core</artifactId>
68+
<scope>test</scope>
69+
</dependency>
70+
</dependencies>
71+
72+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.autoconfigure;
18+
19+
import com.fasterxml.jackson.core.json.JsonReadFeature;
20+
import com.fasterxml.jackson.databind.DeserializationFeature;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.databind.SerializationFeature;
23+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
24+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
25+
import com.fasterxml.jackson.databind.json.JsonMapper;
26+
27+
import org.springframework.ai.model.ModelOptionsUtils;
28+
import org.springframework.ai.util.JacksonUtils;
29+
import org.springframework.ai.util.json.JsonParser;
30+
import org.springframework.boot.autoconfigure.AutoConfiguration;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
33+
import org.springframework.context.annotation.Bean;
34+
35+
/**
36+
* {@link AutoConfiguration Auto-configuration} for Spring AI ObjectMapper beans. Provides
37+
* customizable ObjectMapper beans for JSON parsing and model options handling. Users can
38+
* override these beans to customize JSON processing behavior.
39+
*
40+
* @author Daniel Albuquerque
41+
* @since 2.0.0
42+
*/
43+
@AutoConfiguration
44+
@ConditionalOnClass({ JsonParser.class, ObjectMapper.class })
45+
public class ObjectMapperAutoConfiguration {
46+
47+
/**
48+
* Creates an ObjectMapper bean configured for lenient JSON parsing of LLM responses.
49+
* This ObjectMapper is used by {@link JsonParser} for tool calling and structured
50+
* output.
51+
*
52+
* <p>
53+
* Default configuration:
54+
* <ul>
55+
* <li>Allows unescaped control characters (common in LLM responses)</li>
56+
* <li>Ignores unknown properties</li>
57+
* <li>Serializes dates as ISO strings instead of timestamps</li>
58+
* <li>Allows empty beans to be serialized</li>
59+
* </ul>
60+
*
61+
* <p>
62+
* Users can override this bean by defining their own bean with the name
63+
* "jsonParserObjectMapper".
64+
* @return configured ObjectMapper for JSON parsing
65+
*/
66+
@Bean(name = "jsonParserObjectMapper", defaultCandidate = false)
67+
@ConditionalOnMissingBean(name = "jsonParserObjectMapper")
68+
public ObjectMapper jsonParserObjectMapper() {
69+
ObjectMapper mapper = JsonMapper.builder()
70+
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
71+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
72+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
73+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
74+
.addModules(JacksonUtils.instantiateAvailableModules())
75+
.build();
76+
77+
// Set this ObjectMapper to be used by JsonParser
78+
JsonParser.setObjectMapper(mapper);
79+
80+
return mapper;
81+
}
82+
83+
/**
84+
* Creates an ObjectMapper bean configured for model options
85+
* serialization/deserialization. This ObjectMapper is used by
86+
* {@link ModelOptionsUtils} for converting model options.
87+
*
88+
* <p>
89+
* Default configuration:
90+
* <ul>
91+
* <li>Ignores unknown properties</li>
92+
* <li>Allows empty beans to be serialized</li>
93+
* <li>Accepts empty strings as null objects</li>
94+
* <li>Coerces empty strings to null for Enum types</li>
95+
* </ul>
96+
*
97+
* <p>
98+
* Users can override this bean by defining their own bean with the name
99+
* "modelOptionsObjectMapper".
100+
* @return configured ObjectMapper for model options
101+
*/
102+
@Bean(name = "modelOptionsObjectMapper", defaultCandidate = false)
103+
@ConditionalOnMissingBean(name = "modelOptionsObjectMapper")
104+
public ObjectMapper modelOptionsObjectMapper() {
105+
ObjectMapper mapper = JsonMapper.builder()
106+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
107+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
108+
.addModules(JacksonUtils.instantiateAvailableModules())
109+
.build()
110+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
111+
112+
// Configure coercion for empty strings to null for Enum types
113+
// This fixes the issue where empty string finish_reason values cause
114+
// deserialization failures
115+
mapper.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
116+
117+
// Set this ObjectMapper to be used by ModelOptionsUtils
118+
ModelOptionsUtils.setObjectMapper(mapper);
119+
120+
return mapper;
121+
}
122+
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.ai.model.autoconfigure.ObjectMapperAutoConfiguration
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai-parent</artifactId>
9+
<version>2.0.0-SNAPSHOT</version>
10+
<relativePath>../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-json</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI JSON Auto Configuration</name>
15+
<description>Spring AI JSON Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
25+
<dependencies>
26+
27+
<dependency>
28+
<groupId>org.springframework.ai</groupId>
29+
<artifactId>spring-ai-model</artifactId>
30+
<version>${project.parent.version}</version>
31+
</dependency>
32+
33+
<!-- Boot dependencies -->
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter</artifactId>
37+
</dependency>
38+
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
42+
<optional>true</optional>
43+
</dependency>
44+
45+
<!-- Jackson dependencies -->
46+
<dependency>
47+
<groupId>com.fasterxml.jackson.core</groupId>
48+
<artifactId>jackson-databind</artifactId>
49+
</dependency>
50+
51+
<!-- Test dependencies -->
52+
<dependency>
53+
<groupId>org.springframework.ai</groupId>
54+
<artifactId>spring-ai-test</artifactId>
55+
<version>${project.parent.version}</version>
56+
<scope>test</scope>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>org.springframework.boot</groupId>
61+
<artifactId>spring-boot-starter-test</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
</dependencies>
65+
66+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.autoconfigure.json;
18+
19+
import com.fasterxml.jackson.core.json.JsonReadFeature;
20+
import com.fasterxml.jackson.databind.DeserializationFeature;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.databind.SerializationFeature;
23+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
24+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
25+
import com.fasterxml.jackson.databind.json.JsonMapper;
26+
27+
import org.springframework.ai.model.ModelOptionsUtils;
28+
import org.springframework.ai.util.JacksonUtils;
29+
import org.springframework.ai.util.json.JsonParser;
30+
import org.springframework.boot.autoconfigure.AutoConfiguration;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
33+
import org.springframework.context.annotation.Bean;
34+
35+
/**
36+
* {@link AutoConfiguration Auto-configuration} for Spring AI JSON parsing. Provides
37+
* customizable ObjectMapper beans for JSON parsing and model options handling. Users can
38+
* override these beans to customize JSON processing behavior.
39+
*
40+
* @author Daniel Albuquerque
41+
* @since 2.0.0
42+
*/
43+
@AutoConfiguration
44+
@ConditionalOnClass({ JsonParser.class, ObjectMapper.class })
45+
public class SpringAiJsonAutoConfiguration {
46+
47+
/**
48+
* Creates an ObjectMapper bean configured for lenient JSON parsing of LLM responses.
49+
* This ObjectMapper is used by {@link JsonParser} for tool calling and structured
50+
* output.
51+
*
52+
* <p>
53+
* Default configuration:
54+
* <ul>
55+
* <li>Allows unescaped control characters (common in LLM responses)</li>
56+
* <li>Ignores unknown properties</li>
57+
* <li>Serializes dates as ISO strings instead of timestamps</li>
58+
* <li>Allows empty beans to be serialized</li>
59+
* </ul>
60+
* @return configured ObjectMapper for JSON parsing
61+
*/
62+
@Bean
63+
@ConditionalOnMissingBean(name = "jsonParserObjectMapper")
64+
public ObjectMapper jsonParserObjectMapper() {
65+
ObjectMapper mapper = JsonMapper.builder()
66+
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
67+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
68+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
69+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
70+
.addModules(JacksonUtils.instantiateAvailableModules())
71+
.build();
72+
73+
// Set this ObjectMapper to be used by JsonParser
74+
JsonParser.setObjectMapper(mapper);
75+
76+
return mapper;
77+
}
78+
79+
/**
80+
* Creates an ObjectMapper bean configured for model options
81+
* serialization/deserialization. This ObjectMapper is used by
82+
* {@link ModelOptionsUtils} for converting model options.
83+
*
84+
* <p>
85+
* Default configuration:
86+
* <ul>
87+
* <li>Ignores unknown properties</li>
88+
* <li>Allows empty beans to be serialized</li>
89+
* <li>Accepts empty strings as null objects</li>
90+
* <li>Coerces empty strings to null for Enum types</li>
91+
* </ul>
92+
* @return configured ObjectMapper for model options
93+
*/
94+
@Bean
95+
@ConditionalOnMissingBean(name = "modelOptionsObjectMapper")
96+
public ObjectMapper modelOptionsObjectMapper() {
97+
ObjectMapper mapper = JsonMapper.builder()
98+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
99+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
100+
.addModules(JacksonUtils.instantiateAvailableModules())
101+
.build()
102+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
103+
104+
// Configure coercion for empty strings to null for Enum types
105+
// This fixes the issue where empty string finish_reason values cause
106+
// deserialization failures
107+
mapper.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
108+
109+
// Set this ObjectMapper to be used by ModelOptionsUtils
110+
ModelOptionsUtils.setObjectMapper(mapper);
111+
112+
return mapper;
113+
}
114+
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.ai.autoconfigure.json.SpringAiJsonAutoConfiguration

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383

8484

8585
<module>auto-configurations/common/spring-ai-autoconfigure-retry</module>
86+
<module>auto-configurations/common/spring-ai-autoconfigure-json</module>
8687

8788
<module>auto-configurations/models/tool/spring-ai-autoconfigure-model-tool</module>
8889

0 commit comments

Comments
 (0)