Skip to content

Commit b167f21

Browse files
committed
ModelOptionsUtils: Add ACCEPT_EMPTY_STRING_AS_NULL_OBJECT and ObjectMapper overloads
- Configure OBJECT_MAPPER to accept empty strings as null objects during deserialization. - Add overloaded jsonToMap(String, ObjectMapper) for custom ObjectMapper usage. - Add and update tests to verify correct handling of empty strings for both Map and POJO deserialization, including custom ObjectMapper scenarios. - Clarify documentation and ensure extensibility without global side effects. Fixes #2222 Signed-off-by: Mark Pollack <[email protected]>
1 parent f329d71 commit b167f21

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public abstract class ModelOptionsUtils {
6969
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
7070
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
7171
.addModules(JacksonUtils.instantiateAvailableModules())
72-
.build();
72+
.build()
73+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
7374

7475
private static final List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");
7576

@@ -82,13 +83,25 @@ public abstract class ModelOptionsUtils {
8283
};
8384

8485
/**
85-
* Converts the given JSON string to a Map of String and Object.
86+
* Converts the given JSON string to a Map of String and Object using the default
87+
* ObjectMapper.
8688
* @param json the JSON string to convert to a Map.
8789
* @return the converted Map.
8890
*/
8991
public static Map<String, Object> jsonToMap(String json) {
92+
return jsonToMap(json, OBJECT_MAPPER);
93+
}
94+
95+
/**
96+
* Converts the given JSON string to a Map of String and Object using a custom
97+
* ObjectMapper.
98+
* @param json the JSON string to convert to a Map.
99+
* @param objectMapper the ObjectMapper to use for deserialization.
100+
* @return the converted Map.
101+
*/
102+
public static Map<String, Object> jsonToMap(String json, ObjectMapper objectMapper) {
90103
try {
91-
return OBJECT_MAPPER.readValue(json, MAP_TYPE_REF);
104+
return objectMapper.readValue(json, MAP_TYPE_REF);
92105
}
93106
catch (Exception e) {
94107
throw new RuntimeException(e);

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
import static org.assertj.core.api.Assertions.assertThat;
2525
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2626

27+
import com.fasterxml.jackson.databind.DeserializationFeature;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
import com.fasterxml.jackson.databind.json.JsonMapper;
30+
import com.fasterxml.jackson.databind.SerializationFeature;
31+
2732
/**
2833
* @author Christian Tzolov
2934
*/
@@ -122,6 +127,55 @@ public void copyToTarget() {
122127
assertThat(target.getSpecificField()).isNull();
123128
}
124129

130+
@Test
131+
public void jsonToMap_emptyStringAsNullObject() {
132+
String json = "{\"name\":\"\", \"age\":30}";
133+
// For Map: empty string remains ""
134+
Map<String, Object> map = ModelOptionsUtils.jsonToMap(json);
135+
assertThat(map.get("name")).isEqualTo("");
136+
assertThat(map.get("age")).isEqualTo(30);
137+
138+
// Custom ObjectMapper: still "" for Map
139+
ObjectMapper strictMapper = JsonMapper.builder()
140+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
141+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
142+
.build()
143+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false);
144+
Map<String, Object> mapStrict = ModelOptionsUtils.jsonToMap(json, strictMapper);
145+
assertThat(mapStrict.get("name")).isEqualTo("");
146+
}
147+
148+
@Test
149+
public void pojo_emptyStringAsNullObject() throws Exception {
150+
String json = "{\"name\":\"\", \"age\":30}";
151+
152+
// POJO with default OBJECT_MAPPER (feature enabled)
153+
Person person = ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Person.class);
154+
assertThat(person.name).isEqualTo(""); // String remains ""
155+
assertThat(person.age).isEqualTo(30); // Integer is fine
156+
157+
String jsonWithEmptyAge = "{\"name\":\"John\", \"age\":\"\"}";
158+
Person person2 = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyAge, Person.class);
159+
assertThat(person2.name).isEqualTo("John");
160+
assertThat(person2.age).isNull(); // Integer: "" → null
161+
162+
// POJO with feature disabled: should fail for Integer field
163+
ObjectMapper strictMapper = JsonMapper.builder()
164+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
165+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
166+
.build()
167+
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false);
168+
assertThatThrownBy(() -> strictMapper.readValue(jsonWithEmptyAge, Person.class)).isInstanceOf(Exception.class);
169+
}
170+
171+
public static class Person {
172+
173+
public String name;
174+
175+
public Integer age;
176+
177+
}
178+
125179
@Test
126180
public void getJsonPropertyValues() {
127181
record TestRecord(@JsonProperty("field1") String fieldA, @JsonProperty("field2") String fieldB) {

0 commit comments

Comments
 (0)