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 <mark.pollack@broadcom.com>
This commit is contained in:
Mark Pollack
2025-05-06 18:37:07 -04:00
parent f329d71b7b
commit b167f21e25
2 changed files with 70 additions and 3 deletions

View File

@@ -69,7 +69,8 @@ public abstract class ModelOptionsUtils {
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.addModules(JacksonUtils.instantiateAvailableModules())
.build();
.build()
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
private static final List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");
@@ -82,13 +83,25 @@ public abstract class ModelOptionsUtils {
};
/**
* Converts the given JSON string to a Map of String and Object.
* Converts the given JSON string to a Map of String and Object using the default
* ObjectMapper.
* @param json the JSON string to convert to a Map.
* @return the converted Map.
*/
public static Map<String, Object> jsonToMap(String json) {
return jsonToMap(json, OBJECT_MAPPER);
}
/**
* Converts the given JSON string to a Map of String and Object using a custom
* ObjectMapper.
* @param json the JSON string to convert to a Map.
* @param objectMapper the ObjectMapper to use for deserialization.
* @return the converted Map.
*/
public static Map<String, Object> jsonToMap(String json, ObjectMapper objectMapper) {
try {
return OBJECT_MAPPER.readValue(json, MAP_TYPE_REF);
return objectMapper.readValue(json, MAP_TYPE_REF);
}
catch (Exception e) {
throw new RuntimeException(e);

View File

@@ -24,6 +24,11 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* @author Christian Tzolov
*/
@@ -122,6 +127,55 @@ public class ModelOptionsUtilsTests {
assertThat(target.getSpecificField()).isNull();
}
@Test
public void jsonToMap_emptyStringAsNullObject() {
String json = "{\"name\":\"\", \"age\":30}";
// For Map: empty string remains ""
Map<String, Object> map = ModelOptionsUtils.jsonToMap(json);
assertThat(map.get("name")).isEqualTo("");
assertThat(map.get("age")).isEqualTo(30);
// Custom ObjectMapper: still "" for Map
ObjectMapper strictMapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.build()
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false);
Map<String, Object> mapStrict = ModelOptionsUtils.jsonToMap(json, strictMapper);
assertThat(mapStrict.get("name")).isEqualTo("");
}
@Test
public void pojo_emptyStringAsNullObject() throws Exception {
String json = "{\"name\":\"\", \"age\":30}";
// POJO with default OBJECT_MAPPER (feature enabled)
Person person = ModelOptionsUtils.OBJECT_MAPPER.readValue(json, Person.class);
assertThat(person.name).isEqualTo(""); // String remains ""
assertThat(person.age).isEqualTo(30); // Integer is fine
String jsonWithEmptyAge = "{\"name\":\"John\", \"age\":\"\"}";
Person person2 = ModelOptionsUtils.OBJECT_MAPPER.readValue(jsonWithEmptyAge, Person.class);
assertThat(person2.name).isEqualTo("John");
assertThat(person2.age).isNull(); // Integer: "" → null
// POJO with feature disabled: should fail for Integer field
ObjectMapper strictMapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.build()
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, false);
assertThatThrownBy(() -> strictMapper.readValue(jsonWithEmptyAge, Person.class)).isInstanceOf(Exception.class);
}
public static class Person {
public String name;
public Integer age;
}
@Test
public void getJsonPropertyValues() {
record TestRecord(@JsonProperty("field1") String fieldA, @JsonProperty("field2") String fieldB) {