diff --git a/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializer.java b/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializer.java index 9ebee24..ae4cdff 100644 --- a/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializer.java +++ b/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializer.java @@ -19,6 +19,7 @@ public class JsonNullableDeserializer extends ReferenceTypeDeserializer deser) { + TypeDeserializer typeDeser, JsonDeserializer deser, + boolean mapBlankStringToNull) { super(fullType, inst, typeDeser, deser); + this.mapBlankStringToNull = mapBlankStringToNull; if (fullType instanceof ReferenceType && ((ReferenceType) fullType).getReferencedType() != null) { this.isStringDeserializer = ((ReferenceType) fullType).getReferencedType().isTypeOrSubTypeOf(String.class); } @@ -45,7 +48,7 @@ public JsonNullable deserialize(JsonParser p, DeserializationContext ctx if (t == JsonToken.VALUE_STRING && !isStringDeserializer) { String str = p.getText().trim(); if (str.isEmpty()) { - return JsonNullable.undefined(); + return mapBlankStringToNull ? JsonNullable.of(null) : JsonNullable.undefined(); } } return super.deserialize(p, ctxt); @@ -54,7 +57,7 @@ public JsonNullable deserialize(JsonParser p, DeserializationContext ctx @Override public JsonNullableDeserializer withResolved(TypeDeserializer typeDeser, JsonDeserializer valueDeser) { return new JsonNullableDeserializer(_fullType, _valueInstantiator, - typeDeser, valueDeser); + typeDeser, valueDeser, mapBlankStringToNull); } @Override @@ -92,4 +95,4 @@ public Boolean supportsUpdate(DeserializationConfig config) { // yes; regardless of value deserializer reference itself may be updated return Boolean.TRUE; } -} \ No newline at end of file +} diff --git a/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializers.java b/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializers.java index 04764e9..e33990a 100644 --- a/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializers.java +++ b/src/main/java/org/openapitools/jackson/nullable/JsonNullableDeserializers.java @@ -9,10 +9,18 @@ public class JsonNullableDeserializers extends Deserializers.Base { + private final boolean mapBlankStringToNull; + + public JsonNullableDeserializers(boolean mapBlankStringToNull) { + this.mapBlankStringToNull = mapBlankStringToNull; + } + @Override public JsonDeserializer findReferenceDeserializer(ReferenceType refType, DeserializationConfig config, BeanDescription beanDesc, TypeDeserializer contentTypeDeserializer, JsonDeserializer contentDeserializer) { - return (refType.hasRawClass(JsonNullable.class)) ? new JsonNullableDeserializer(refType, null, contentTypeDeserializer,contentDeserializer) : null; + return (refType.hasRawClass(JsonNullable.class)) + ? new JsonNullableDeserializer(refType, null, contentTypeDeserializer, contentDeserializer, mapBlankStringToNull) + : null; } } diff --git a/src/main/java/org/openapitools/jackson/nullable/JsonNullableModule.java b/src/main/java/org/openapitools/jackson/nullable/JsonNullableModule.java index 2101f83..08a7535 100644 --- a/src/main/java/org/openapitools/jackson/nullable/JsonNullableModule.java +++ b/src/main/java/org/openapitools/jackson/nullable/JsonNullableModule.java @@ -7,11 +7,27 @@ public class JsonNullableModule extends Module { private final String NAME = "JsonNullableModule"; + private boolean mapBlankStringToNull = false; + + /** + * Configures whether blank strings (e.g. {@code ""}, {@code " "}) deserialized into + * non-String {@code JsonNullable} fields are mapped to {@code JsonNullable.of(null)} + * instead of {@code JsonNullable.undefined()}. + * + *

This is relevant for PATCH semantics: a blank string sent by a client expresses + * explicit intent to clear a value, which {@code undefined()} silently swallows. + * + *

Default is {@code false} for backwards compatibility. + */ + public JsonNullableModule mapBlankStringToNull(boolean state) { + this.mapBlankStringToNull = state; + return this; + } @Override public void setupModule(SetupContext context) { context.addSerializers(new JsonNullableSerializers()); - context.addDeserializers(new JsonNullableDeserializers()); + context.addDeserializers(new JsonNullableDeserializers(mapBlankStringToNull)); // Modify type info for JsonNullable context.addTypeModifier(new JsonNullableTypeModifier()); context.addBeanSerializerModifier(new JsonNullableBeanSerializerModifier()); diff --git a/src/test/java/org/openapitools/jackson/nullable/JsonNullWithEmptyTest.java b/src/test/java/org/openapitools/jackson/nullable/JsonNullWithEmptyTest.java index 2308246..fcfe807 100644 --- a/src/test/java/org/openapitools/jackson/nullable/JsonNullWithEmptyTest.java +++ b/src/test/java/org/openapitools/jackson/nullable/JsonNullWithEmptyTest.java @@ -6,10 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class JsonNullWithEmptyTest extends ModuleTestBase { private final ObjectMapper MAPPER = mapperWithModule(); + private final ObjectMapper MAPPER_BLANK_TO_NULL = mapperWithModule(new JsonNullableModule().mapBlankStringToNull(true)); static class BooleanBean { public JsonNullable value; @@ -20,6 +23,8 @@ public BooleanBean(Boolean b) { } } + // default behavior (mapBlankStringToNull = false) + @Test void testJsonNullableFromEmpty() throws Exception { JsonNullable value = MAPPER.readValue(quote(""), new TypeReference>() {}); @@ -37,4 +42,27 @@ void testBooleanWithEmpty() throws Exception { assertFalse(b.value.isPresent()); } -} \ No newline at end of file + // mapBlankStringToNull = true + + @Test + void testJsonNullableFromEmptyWithMapBlankStringToNull() throws Exception { + JsonNullable value = MAPPER_BLANK_TO_NULL.readValue(quote(""), new TypeReference>() {}); + assertTrue(value.isPresent()); + assertNull(value.get()); + } + + @Test + void testJsonNullableFromBlankWithMapBlankStringToNull() throws Exception { + JsonNullable value = MAPPER_BLANK_TO_NULL.readValue(quote(" "), new TypeReference>() {}); + assertTrue(value.isPresent()); + assertNull(value.get()); + } + + @Test + void testBooleanWithEmptyWithMapBlankStringToNull() throws Exception { + BooleanBean b = MAPPER_BLANK_TO_NULL.readValue(aposToQuotes("{'value':''}"), BooleanBean.class); + assertNotNull(b.value); + assertTrue(b.value.isPresent()); + assertNull(b.value.get()); + } +} diff --git a/src/test/java/org/openapitools/jackson/nullable/JsonNullableBasicTest.java b/src/test/java/org/openapitools/jackson/nullable/JsonNullableBasicTest.java index 13c947c..03823de 100644 --- a/src/test/java/org/openapitools/jackson/nullable/JsonNullableBasicTest.java +++ b/src/test/java/org/openapitools/jackson/nullable/JsonNullableBasicTest.java @@ -60,6 +60,7 @@ public static class ContainedImpl implements Contained { } */ private final ObjectMapper MAPPER = mapperWithModule(); + private final ObjectMapper MAPPER_BLANK_TO_NULL = mapperWithModule(new JsonNullableModule().mapBlankStringToNull(true)); @Test void testJsonNullableTypeResolution() { @@ -270,11 +271,18 @@ void testJsonNullableCollection() throws Exception { } @Test - void testDeserNull() throws Exception { + void testDeserEmptyStringForNonStringType() throws Exception { JsonNullable value = MAPPER.readValue("\"\"", new TypeReference>() {}); assertFalse(value.isPresent()); } + @Test + void testDeserEmptyStringForNonStringTypeWithMapBlankStringToNull() throws Exception { + JsonNullable value = MAPPER_BLANK_TO_NULL.readValue("\"\"", new TypeReference>() {}); + assertTrue(value.isPresent()); + assertNull(value.get()); + } + @Test void testPolymorphic() throws Exception { final Container dto = new Container(); diff --git a/src/test/java/org/openapitools/jackson/nullable/ModuleTestBase.java b/src/test/java/org/openapitools/jackson/nullable/ModuleTestBase.java index 102c585..7c591e7 100644 --- a/src/test/java/org/openapitools/jackson/nullable/ModuleTestBase.java +++ b/src/test/java/org/openapitools/jackson/nullable/ModuleTestBase.java @@ -21,6 +21,13 @@ protected ObjectMapper mapperWithModule() return mapper; } + protected ObjectMapper mapperWithModule(JsonNullableModule module) + { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + return mapper; + } + /* /********************************************************************** /* Helper methods, setup @@ -47,4 +54,4 @@ protected String quote(String str) { protected String aposToQuotes(String json) { return json.replace("'", "\""); } -} \ No newline at end of file +}