Skip to content

Commit 1b0583c

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

File tree

3 files changed

+55
-36
lines changed

3 files changed

+55
-36
lines changed

spring-ai-model/src/main/java/org/springframework/ai/model/ModelOptionsUtils.java

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,31 @@
6767
*/
6868
public abstract class ModelOptionsUtils {
6969

70-
public static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
71-
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
72-
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
73-
.addModules(JacksonUtils.instantiateAvailableModules())
74-
.build()
75-
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
70+
private static final ObjectMapper DEFAULT_OBJECT_MAPPER;
7671

7772
static {
73+
DEFAULT_OBJECT_MAPPER = JsonMapper.builder()
74+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
75+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
76+
.addModules(JacksonUtils.instantiateAvailableModules())
77+
.build()
78+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
79+
7880
// Configure coercion for empty strings to null for Enum types
7981
// This fixes the issue where empty string finish_reason values cause
8082
// deserialization failures
81-
OBJECT_MAPPER.coercionConfigFor(Enum.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
83+
DEFAULT_OBJECT_MAPPER.coercionConfigFor(Enum.class)
84+
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
85+
}
86+
87+
private static final AtomicReference<ObjectMapper> OBJECT_MAPPER = new AtomicReference<>(DEFAULT_OBJECT_MAPPER);
88+
89+
/**
90+
* Returns the {@link ObjectMapper} instance used for model options operations.
91+
* @return the ObjectMapper instance
92+
*/
93+
public static ObjectMapper getObjectMapper() {
94+
return OBJECT_MAPPER.get();
8295
}
8396

8497
private static final List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");
@@ -98,7 +111,7 @@ public abstract class ModelOptionsUtils {
98111
* @return the converted Map.
99112
*/
100113
public static Map<String, Object> jsonToMap(String json) {
101-
return jsonToMap(json, OBJECT_MAPPER);
114+
return jsonToMap(json, OBJECT_MAPPER.get());
102115
}
103116

104117
/**
@@ -126,7 +139,7 @@ public static Map<String, Object> jsonToMap(String json, ObjectMapper objectMapp
126139
*/
127140
public static <T> T jsonToObject(String json, Class<T> type) {
128141
try {
129-
return OBJECT_MAPPER.readValue(json, type);
142+
return OBJECT_MAPPER.get().readValue(json, type);
130143
}
131144
catch (Exception e) {
132145
throw new RuntimeException("Failed to json: " + json, e);
@@ -140,7 +153,7 @@ public static <T> T jsonToObject(String json, Class<T> type) {
140153
*/
141154
public static String toJsonString(Object object) {
142155
try {
143-
return OBJECT_MAPPER.writeValueAsString(object);
156+
return OBJECT_MAPPER.get().writeValueAsString(object);
144157
}
145158
catch (JsonProcessingException e) {
146159
throw new RuntimeException(e);
@@ -154,7 +167,7 @@ public static String toJsonString(Object object) {
154167
*/
155168
public static String toJsonStringPrettyPrinter(Object object) {
156169
try {
157-
return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object);
170+
return OBJECT_MAPPER.get().writerWithDefaultPrettyPrinter().writeValueAsString(object);
158171
}
159172
catch (JsonProcessingException e) {
160173
throw new RuntimeException(e);
@@ -231,8 +244,9 @@ public static Map<String, Object> objectToMap(Object source) {
231244
return new HashMap<>();
232245
}
233246
try {
234-
String json = OBJECT_MAPPER.writeValueAsString(source);
235-
return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {
247+
ObjectMapper mapper = OBJECT_MAPPER.get();
248+
String json = mapper.writeValueAsString(source);
249+
return mapper.readValue(json, new TypeReference<Map<String, Object>>() {
236250

237251
})
238252
.entrySet()
@@ -254,8 +268,9 @@ public static Map<String, Object> objectToMap(Object source) {
254268
*/
255269
public static <T> T mapToClass(Map<String, Object> source, Class<T> clazz) {
256270
try {
257-
String json = OBJECT_MAPPER.writeValueAsString(source);
258-
return OBJECT_MAPPER.readValue(json, clazz);
271+
ObjectMapper mapper = OBJECT_MAPPER.get();
272+
String json = mapper.writeValueAsString(source);
273+
return mapper.readValue(json, clazz);
259274
}
260275
catch (JsonProcessingException e) {
261276
throw new RuntimeException(e);

spring-ai-model/src/main/java/org/springframework/ai/util/json/JsonParser.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.reflect.Type;
2020
import java.math.BigDecimal;
21+
import java.util.concurrent.atomic.AtomicReference;
2122

2223
import com.fasterxml.jackson.core.JsonProcessingException;
2324
import com.fasterxml.jackson.core.type.TypeReference;
@@ -36,12 +37,14 @@
3637
*/
3738
public final class JsonParser {
3839

39-
private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
40+
private static final ObjectMapper DEFAULT_OBJECT_MAPPER = JsonMapper.builder()
4041
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
4142
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
4243
.addModules(JacksonUtils.instantiateAvailableModules())
4344
.build();
4445

46+
private static final AtomicReference<ObjectMapper> OBJECT_MAPPER = new AtomicReference<>(DEFAULT_OBJECT_MAPPER);
47+
4548
private JsonParser() {
4649
}
4750

@@ -50,7 +53,7 @@ private JsonParser() {
5053
* operations for tool calling and structured output.
5154
*/
5255
public static ObjectMapper getObjectMapper() {
53-
return OBJECT_MAPPER;
56+
return OBJECT_MAPPER.get();
5457
}
5558

5659
/**
@@ -61,7 +64,7 @@ public static <T> T fromJson(String json, Class<T> type) {
6164
Assert.notNull(type, "type cannot be null");
6265

6366
try {
64-
return OBJECT_MAPPER.readValue(json, type);
67+
return OBJECT_MAPPER.get().readValue(json, type);
6568
}
6669
catch (JsonProcessingException ex) {
6770
throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getName()), ex);
@@ -76,7 +79,8 @@ public static <T> T fromJson(String json, Type type) {
7679
Assert.notNull(type, "type cannot be null");
7780

7881
try {
79-
return OBJECT_MAPPER.readValue(json, OBJECT_MAPPER.constructType(type));
82+
ObjectMapper mapper = OBJECT_MAPPER.get();
83+
return mapper.readValue(json, mapper.constructType(type));
8084
}
8185
catch (JsonProcessingException ex) {
8286
throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getTypeName()), ex);
@@ -91,7 +95,7 @@ public static <T> T fromJson(String json, TypeReference<T> type) {
9195
Assert.notNull(type, "type cannot be null");
9296

9397
try {
94-
return OBJECT_MAPPER.readValue(json, type);
98+
return OBJECT_MAPPER.get().readValue(json, type);
9599
}
96100
catch (JsonProcessingException ex) {
97101
throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getType().getTypeName()),
@@ -104,7 +108,7 @@ public static <T> T fromJson(String json, TypeReference<T> type) {
104108
*/
105109
private static boolean isValidJson(String input) {
106110
try {
107-
OBJECT_MAPPER.readTree(input);
111+
OBJECT_MAPPER.get().readTree(input);
108112
return true;
109113
}
110114
catch (JsonProcessingException e) {
@@ -120,7 +124,7 @@ public static String toJson(@Nullable Object object) {
120124
return str;
121125
}
122126
try {
123-
return OBJECT_MAPPER.writeValueAsString(object);
127+
return OBJECT_MAPPER.get().writeValueAsString(object);
124128
}
125129
catch (JsonProcessingException ex) {
126130
throw new IllegalStateException("Conversion from Object to JSON failed", ex);

spring-ai-model/src/test/java/org/springframework/ai/model/ModelOptionsUtilsTests.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ public void pojo_emptyStringAsNullObject() throws Exception {
150150
String json = "{\"name\":\"\", \"age\":30}";
151151

152152
// POJO with default OBJECT_MAPPER (feature enabled)
153-
Person person = ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Person.class);
153+
Person person = ModelOptionsUtils.getObjectMapper().readValue(json, Person.class);
154154
assertThat(person.name).isEqualTo(""); // String remains ""
155155
assertThat(person.age).isEqualTo(30); // Integer is fine
156156

157157
String jsonWithEmptyAge = "{\"name\":\"John\", \"age\":\"\"}";
158-
Person person2 = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyAge, Person.class);
158+
Person person2 = ModelOptionsUtils.getObjectMapper().readValue(jsonWithEmptyAge, Person.class);
159159
assertThat(person2.name).isEqualTo("John");
160160
assertThat(person2.age).isNull(); // Integer: "" → null
161161

@@ -182,35 +182,35 @@ record TestRecord(@JsonProperty("field1") String fieldA, @JsonProperty("field2")
182182
@Test
183183
public void enumCoercion_emptyStringAsNull() throws JsonProcessingException {
184184
// Test direct enum deserialization with empty string
185-
ColorEnum colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"\"", ColorEnum.class);
185+
ColorEnum colorEnum = ModelOptionsUtils.getObjectMapper().readValue("\"\"", ColorEnum.class);
186186
assertThat(colorEnum).isNull();
187187

188188
// Test direct enum deserialization with valid value
189-
colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"RED\"", ColorEnum.class);
189+
colorEnum = ModelOptionsUtils.getObjectMapper().readValue("\"RED\"", ColorEnum.class);
190190
assertThat(colorEnum).isEqualTo(ColorEnum.RED);
191191

192192
// Test direct enum deserialization with invalid value should throw exception
193193
final String jsonInvalid = "\"Invalid\"";
194-
assertThatThrownBy(() -> ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonInvalid, ColorEnum.class))
194+
assertThatThrownBy(() -> ModelOptionsUtils.getObjectMapper().readValue(jsonInvalid, ColorEnum.class))
195195
.isInstanceOf(JsonProcessingException.class);
196196
}
197197

198198
@Test
199199
public void enumCoercion_objectMapperConfiguration() throws JsonProcessingException {
200-
// Test that ModelOptionsUtils.OBJECT_MAPPER has the correct coercion
200+
// Test that ModelOptionsUtils.getObjectMapper() has the correct coercion
201201
// configuration
202202
// This validates that our static configuration block is working
203203

204204
// Empty string should coerce to null for enums
205-
ColorEnum colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"\"", ColorEnum.class);
205+
ColorEnum colorEnum = ModelOptionsUtils.getObjectMapper().readValue("\"\"", ColorEnum.class);
206206
assertThat(colorEnum).isNull();
207207

208208
// Null should remain null
209-
colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("null", ColorEnum.class);
209+
colorEnum = ModelOptionsUtils.getObjectMapper().readValue("null", ColorEnum.class);
210210
assertThat(colorEnum).isNull();
211211

212212
// Valid enum values should deserialize correctly
213-
colorEnum = ModelOptionsUtils.OBJECT_MAPPER.readValue("\"BLUE\"", ColorEnum.class);
213+
colorEnum = ModelOptionsUtils.getObjectMapper().readValue("\"BLUE\"", ColorEnum.class);
214214
assertThat(colorEnum).isEqualTo(ColorEnum.BLUE);
215215
}
216216

@@ -224,8 +224,8 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce
224224
}
225225
""";
226226

227-
TestApiResponse response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyFinishReason,
228-
TestApiResponse.class);
227+
TestApiResponse response = ModelOptionsUtils.getObjectMapper()
228+
.readValue(jsonWithEmptyFinishReason, TestApiResponse.class);
229229
assertThat(response.id()).isEqualTo("test-123");
230230
assertThat(response.finishReason()).isNull();
231231

@@ -238,7 +238,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce
238238
}
239239
""";
240240

241-
response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithValidFinishReason, TestApiResponse.class);
241+
response = ModelOptionsUtils.getObjectMapper().readValue(jsonWithValidFinishReason, TestApiResponse.class);
242242
assertThat(response.id()).isEqualTo("test-456");
243243
assertThat(response.finishReason()).isEqualTo(TestFinishReason.STOP);
244244

@@ -250,7 +250,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce
250250
}
251251
""";
252252

253-
response = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithNullFinishReason, TestApiResponse.class);
253+
response = ModelOptionsUtils.getObjectMapper().readValue(jsonWithNullFinishReason, TestApiResponse.class);
254254
assertThat(response.id()).isEqualTo("test-789");
255255
assertThat(response.finishReason()).isNull();
256256

@@ -263,7 +263,7 @@ public void enumCoercion_apiResponseWithFinishReason() throws JsonProcessingExce
263263
""";
264264

265265
assertThatThrownBy(
266-
() -> ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithInvalidFinishReason, TestApiResponse.class))
266+
() -> ModelOptionsUtils.getObjectMapper().readValue(jsonWithInvalidFinishReason, TestApiResponse.class))
267267
.isInstanceOf(JsonProcessingException.class)
268268
.hasMessageContaining("INVALID_VALUE");
269269
}

0 commit comments

Comments
 (0)