Skip to content

Commit 79650bb

Browse files
committed
Allow using spring's object mapper
Signed-off-by: Daniel Albuquerque <daniel.albuquerque@teya.com>
1 parent 47e4232 commit 79650bb

File tree

10 files changed

+856
-18
lines changed

10 files changed

+856
-18
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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>1.1.0-SNAPSHOT</version>
10+
<relativePath>../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-json-parser</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI JsonParser Auto Configuration</name>
15+
<description>Spring AI JsonParser 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+
<dependencies>
25+
26+
<dependency>
27+
<groupId>org.springframework.ai</groupId>
28+
<artifactId>spring-ai-model</artifactId>
29+
<version>${project.parent.version}</version>
30+
</dependency>
31+
32+
<!-- Boot dependencies -->
33+
<dependency>
34+
<groupId>org.springframework.boot</groupId>
35+
<artifactId>spring-boot-starter</artifactId>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>org.springframework.boot</groupId>
40+
<artifactId>spring-boot-configuration-processor</artifactId>
41+
<optional>true</optional>
42+
</dependency>
43+
44+
<dependency>
45+
<groupId>org.springframework.boot</groupId>
46+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
47+
<optional>true</optional>
48+
</dependency>
49+
50+
<!-- Jackson -->
51+
<dependency>
52+
<groupId>com.fasterxml.jackson.core</groupId>
53+
<artifactId>jackson-databind</artifactId>
54+
</dependency>
55+
56+
<!-- Test dependencies -->
57+
<dependency>
58+
<groupId>org.springframework.ai</groupId>
59+
<artifactId>spring-ai-test</artifactId>
60+
<version>${project.parent.version}</version>
61+
<scope>test</scope>
62+
</dependency>
63+
64+
<dependency>
65+
<groupId>org.springframework.boot</groupId>
66+
<artifactId>spring-boot-starter-test</artifactId>
67+
<scope>test</scope>
68+
</dependency>
69+
70+
<dependency>
71+
<groupId>org.mockito</groupId>
72+
<artifactId>mockito-core</artifactId>
73+
<scope>test</scope>
74+
</dependency>
75+
</dependencies>
76+
77+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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.util.json.autoconfigure;
18+
19+
import com.fasterxml.jackson.databind.DeserializationFeature;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.fasterxml.jackson.databind.SerializationFeature;
22+
import com.fasterxml.jackson.databind.cfg.CoercionAction;
23+
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
24+
import com.fasterxml.jackson.databind.json.JsonMapper;
25+
26+
import org.springframework.ai.model.ModelOptionsUtils;
27+
import org.springframework.ai.util.JacksonUtils;
28+
import org.springframework.ai.util.json.JsonParser;
29+
import org.springframework.beans.factory.annotation.Qualifier;
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.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.context.annotation.Bean;
35+
36+
/**
37+
* Auto-configuration for JsonParser and ModelOptionsUtils ObjectMapper.
38+
* <p>
39+
* Provides customizable ObjectMappers for JSON parsing operations in tool calling,
40+
* structured output, and model options handling. Users can override these beans to
41+
* customize Jackson behavior.
42+
*
43+
* @author Daniel Albuquerque
44+
*/
45+
@AutoConfiguration
46+
@ConditionalOnClass(ObjectMapper.class)
47+
@EnableConfigurationProperties(JsonParserProperties.class)
48+
public class JsonParserObjectMapperAutoConfiguration {
49+
50+
/**
51+
* Creates a configured ObjectMapper for JsonParser operations.
52+
* <p>
53+
* This ObjectMapper is configured with:
54+
* <ul>
55+
* <li>Lenient deserialization (doesn't fail on unknown properties)</li>
56+
* <li>Proper handling of empty beans during serialization</li>
57+
* <li>Standard Jackson modules (Java 8, JSR-310, ParameterNames, Kotlin)</li>
58+
* <li>Optional features from JsonParserProperties</li>
59+
* </ul>
60+
*
61+
* To customize, provide your own bean: <pre>{@code
62+
* &#64;Bean
63+
* public ObjectMapper jsonParserObjectMapper() {
64+
* return JsonMapper.builder()
65+
* .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
66+
* .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
67+
* .build();
68+
* }
69+
* }</pre>
70+
* @param properties the JsonParser configuration properties
71+
* @return the configured ObjectMapper
72+
*/
73+
@Bean(name = "jsonParserObjectMapper", defaultCandidate = false)
74+
@ConditionalOnMissingBean(name = "jsonParserObjectMapper")
75+
public ObjectMapper jsonParserObjectMapper(JsonParserProperties properties) {
76+
JsonMapper.Builder builder = JsonMapper.builder()
77+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
78+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
79+
.addModules(JacksonUtils.instantiateAvailableModules());
80+
81+
// Apply properties
82+
if (properties.isAllowUnescapedControlChars()) {
83+
builder.enable(com.fasterxml.jackson.core.json.JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS);
84+
}
85+
86+
if (!properties.isWriteDatesAsTimestamps()) {
87+
builder.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
88+
}
89+
90+
return builder.build();
91+
}
92+
93+
/**
94+
* Creates a configured ObjectMapper for ModelOptionsUtils operations.
95+
* <p>
96+
* This ObjectMapper is configured with:
97+
* <ul>
98+
* <li>Lenient deserialization (doesn't fail on unknown properties)</li>
99+
* <li>Proper handling of empty beans during serialization</li>
100+
* <li>Standard Jackson modules (Java 8, JSR-310, ParameterNames, Kotlin)</li>
101+
* <li>Empty string to null coercion for objects and enums</li>
102+
* <li>Optional features from JsonParserProperties</li>
103+
* </ul>
104+
*
105+
* To customize, provide your own bean: <pre>{@code
106+
* &#64;Bean
107+
* public ObjectMapper modelOptionsObjectMapper() {
108+
* ObjectMapper mapper = JsonMapper.builder()
109+
* .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
110+
* .addModules(JacksonUtils.instantiateAvailableModules())
111+
* .build()
112+
* .configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
113+
*
114+
* mapper.coercionConfigFor(Enum.class)
115+
* .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
116+
*
117+
* return mapper;
118+
* }
119+
* }</pre>
120+
* @param properties the configuration properties
121+
* @return the configured ObjectMapper
122+
*/
123+
@Bean(name = "modelOptionsObjectMapper", defaultCandidate = false)
124+
@ConditionalOnMissingBean(name = "modelOptionsObjectMapper")
125+
public ObjectMapper modelOptionsObjectMapper(JsonParserProperties properties) {
126+
JsonMapper.Builder builder = JsonMapper.builder();
127+
128+
// Apply base configuration
129+
if (properties.isFailOnUnknownProperties()) {
130+
builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
131+
}
132+
else {
133+
builder.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
134+
}
135+
136+
if (properties.isFailOnEmptyBeans()) {
137+
builder.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
138+
}
139+
else {
140+
builder.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
141+
}
142+
143+
builder.addModules(JacksonUtils.instantiateAvailableModules());
144+
145+
ObjectMapper mapper = builder.build();
146+
147+
// Configure empty string handling
148+
if (properties.isAcceptEmptyStringAsNull()) {
149+
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
150+
}
151+
152+
// Configure enum coercion (critical for API compatibility)
153+
if (properties.isCoerceEmptyEnumStrings()) {
154+
mapper.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
155+
}
156+
157+
return mapper;
158+
}
159+
160+
/**
161+
* Creates a JsonParser bean configured with the custom ObjectMapper. This also sets
162+
* the static configured mapper for backward compatibility with code using static
163+
* methods.
164+
* @param objectMapper the configured ObjectMapper
165+
* @return the JsonParser instance
166+
*/
167+
@Bean
168+
@ConditionalOnMissingBean
169+
public JsonParser jsonParser(@Qualifier("jsonParserObjectMapper") ObjectMapper objectMapper) {
170+
// Set the static mapper for backward compatibility
171+
JsonParser.setConfiguredObjectMapper(objectMapper);
172+
173+
// Create bean instance
174+
return new JsonParser(objectMapper);
175+
}
176+
177+
/**
178+
* Initializes ModelOptionsUtils with the Spring-managed ObjectMapper. This setter
179+
* allows ModelOptionsUtils static methods to use the Spring-configured mapper while
180+
* maintaining backward compatibility.
181+
* @param objectMapper the configured ObjectMapper for model options
182+
*/
183+
@Bean
184+
@ConditionalOnMissingBean(name = "modelOptionsUtilsInitializer")
185+
public Object modelOptionsUtilsInitializer(@Qualifier("modelOptionsObjectMapper") ObjectMapper objectMapper) {
186+
// Set the static mapper for backward compatibility
187+
ModelOptionsUtils.setConfiguredObjectMapper(objectMapper);
188+
189+
// Return a marker object to satisfy bean contract
190+
return new Object();
191+
}
192+
193+
}
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.util.json.autoconfigure;
18+
19+
import org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* Configuration properties for JsonParser and ModelOptionsUtils ObjectMapper.
23+
*
24+
* @author Daniel Albuquerque
25+
*/
26+
@ConfigurationProperties(prefix = JsonParserProperties.CONFIG_PREFIX)
27+
public class JsonParserProperties {
28+
29+
public static final String CONFIG_PREFIX = "spring.ai.json";
30+
31+
/**
32+
* Allow unescaped control characters (like \n) in JSON strings. Useful when LLMs
33+
* generate JSON with literal newlines.
34+
*/
35+
private boolean allowUnescapedControlChars = false;
36+
37+
/**
38+
* Write dates as ISO-8601 strings instead of timestamp arrays. When false (default),
39+
* dates are written as strings like "2025-07-03".
40+
*/
41+
private boolean writeDatesAsTimestamps = false;
42+
43+
/**
44+
* Accept empty strings as null objects during deserialization. Used by
45+
* ModelOptionsUtils for handling API responses.
46+
*/
47+
private boolean acceptEmptyStringAsNull = true;
48+
49+
/**
50+
* Coerce empty strings to null for enum types. Critical for handling API responses
51+
* with empty finish_reason values.
52+
*/
53+
private boolean coerceEmptyEnumStrings = true;
54+
55+
/**
56+
* Fail on unknown properties during deserialization. When false (default), unknown
57+
* properties are ignored.
58+
*/
59+
private boolean failOnUnknownProperties = false;
60+
61+
/**
62+
* Fail on empty beans during serialization. When false (default), empty beans are
63+
* serialized as empty objects.
64+
*/
65+
private boolean failOnEmptyBeans = false;
66+
67+
public boolean isAllowUnescapedControlChars() {
68+
return this.allowUnescapedControlChars;
69+
}
70+
71+
public void setAllowUnescapedControlChars(boolean allowUnescapedControlChars) {
72+
this.allowUnescapedControlChars = allowUnescapedControlChars;
73+
}
74+
75+
public boolean isWriteDatesAsTimestamps() {
76+
return this.writeDatesAsTimestamps;
77+
}
78+
79+
public void setWriteDatesAsTimestamps(boolean writeDatesAsTimestamps) {
80+
this.writeDatesAsTimestamps = writeDatesAsTimestamps;
81+
}
82+
83+
public boolean isAcceptEmptyStringAsNull() {
84+
return this.acceptEmptyStringAsNull;
85+
}
86+
87+
public void setAcceptEmptyStringAsNull(boolean acceptEmptyStringAsNull) {
88+
this.acceptEmptyStringAsNull = acceptEmptyStringAsNull;
89+
}
90+
91+
public boolean isCoerceEmptyEnumStrings() {
92+
return this.coerceEmptyEnumStrings;
93+
}
94+
95+
public void setCoerceEmptyEnumStrings(boolean coerceEmptyEnumStrings) {
96+
this.coerceEmptyEnumStrings = coerceEmptyEnumStrings;
97+
}
98+
99+
public boolean isFailOnUnknownProperties() {
100+
return this.failOnUnknownProperties;
101+
}
102+
103+
public void setFailOnUnknownProperties(boolean failOnUnknownProperties) {
104+
this.failOnUnknownProperties = failOnUnknownProperties;
105+
}
106+
107+
public boolean isFailOnEmptyBeans() {
108+
return this.failOnEmptyBeans;
109+
}
110+
111+
public void setFailOnEmptyBeans(boolean failOnEmptyBeans) {
112+
this.failOnEmptyBeans = failOnEmptyBeans;
113+
}
114+
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.ai.util.json.autoconfigure.JsonParserObjectMapperAutoConfiguration

0 commit comments

Comments
 (0)