diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 5b6762f9d4..1e8dc2213f 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1863,6 +1863,9 @@ wrongwrong (@k163377) * Contributed fix for #5139: In `CollectionDeserializer`, `JsonSetter.contentNulls` is sometimes ignored (2.19.1) + * Contributed fix for #5202: #5202: `JsonSetter.contentNulls` ignored for `Object[]`, + `String[]` and `Collection` + (2.19.2) Bernd Ahlers (@bernd) * Reported #4742: Deserialization with Builder, External type id, `@JsonCreator` failing diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index fb41822544..ea5e567193 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,9 @@ Project: jackson-databind 2.19.2 (not yet released) +#5202: `JsonSetter.contentNulls` ignored for `Object[]`, `String[]` + and `Collection` + (fix by @wrongwrong) #5215: `@JsonAnyGetter` serialization order change from 2.18.4 to 2.19.0 (reported by EddĂș M) (fix by Joo-Hyuk K) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java index 61cc466335..d6e194c634 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java @@ -211,10 +211,19 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) if (_skipNullValues) { continue; } - value = _nullProvider.getNullValue(ctxt); + value = null; } else { value = _deserializeNoNullChecks(p, ctxt); } + + if (value == null) { + value = _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + if (ix >= chunk.length) { chunk = buffer.appendCompletedChunk(chunk); ix = 0; @@ -275,10 +284,19 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, if (_skipNullValues) { continue; } - value = _nullProvider.getNullValue(ctxt); + value = null; } else { value = _deserializeNoNullChecks(p, ctxt); } + + if (value == null) { + value = _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + if (ix >= chunk.length) { chunk = buffer.appendCompletedChunk(chunk); ix = 0; @@ -346,7 +364,7 @@ protected Object handleNonArray(JsonParser p, DeserializationContext ctxt) if (_skipNullValues) { return _emptyValue; } - value = _nullProvider.getNullValue(ctxt); + value = null; } else { if (p.hasToken(JsonToken.VALUE_STRING)) { String textValue = p.getText(); @@ -371,6 +389,15 @@ protected Object handleNonArray(JsonParser p, DeserializationContext ctxt) value = _deserializeNoNullChecks(p, ctxt); } + + if (value == null) { + value = _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + return _emptyValue; + } + } + // Ok: bit tricky, since we may want T[], not just Object[] Object[] result; @@ -399,4 +426,3 @@ protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext c return _elementDeserializer.deserializeWithType(p, ctxt, _elementTypeDeserializer); } } - diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java index 864299d326..aafdc80230 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java @@ -162,10 +162,17 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt) throws IO if (_skipNullValues) { continue; } - value = (String) _nullProvider.getNullValue(ctxt); } else { value = _parseString(p, ctxt, _nullProvider); } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } } if (ix >= chunk.length) { chunk = buffer.appendCompletedChunk(chunk); @@ -219,13 +226,22 @@ private String[] _deserializeCustom(JsonParser p, DeserializationContext ctxt, if (_skipNullValues) { continue; } - value = (String) _nullProvider.getNullValue(ctxt); + value = null; } else { value = deser.deserialize(p, ctxt); } } else { value = deser.deserialize(p, ctxt); } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + if (ix >= chunk.length) { chunk = buffer.appendCompletedChunk(chunk); ix = 0; @@ -283,10 +299,17 @@ public String[] deserialize(JsonParser p, DeserializationContext ctxt, if (_skipNullValues) { return NO_STRINGS; } - value = (String) _nullProvider.getNullValue(ctxt); } else { value = _parseString(p, ctxt, _nullProvider); } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } } if (ix >= chunk.length) { chunk = buffer.appendCompletedChunk(chunk); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java index d4984a084b..acfb21f32a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java @@ -213,10 +213,18 @@ public Collection deserialize(JsonParser p, DeserializationContext ctxt, if (_skipNullValues) { continue; } - value = (String) _nullProvider.getNullValue(ctxt); } else { value = _parseString(p, ctxt, _nullProvider); } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + result.add(value); } } catch (Exception e) { @@ -246,13 +254,22 @@ private Collection deserializeUsingCustom(JsonParser p, DeserializationC if (_skipNullValues) { continue; } - value = (String) _nullProvider.getNullValue(ctxt); + value = null; } else { value = deser.deserialize(p, ctxt); } } else { value = deser.deserialize(p, ctxt); } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + result.add(value); } } catch (Exception e) { @@ -297,7 +314,7 @@ private final Collection handleNonArray(JsonParser p, DeserializationCon if (_skipNullValues) { return result; } - value = (String) _nullProvider.getNullValue(ctxt); + value = null; } else { if (p.hasToken(JsonToken.VALUE_STRING)) { String textValue = p.getText(); @@ -326,6 +343,15 @@ private final Collection handleNonArray(JsonParser p, DeserializationCon throw JsonMappingException.wrapWithPath(e, result, result.size()); } } + + if (value == null) { + value = (String) _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + return result; + } + } + result.add(value); return result; } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/ObjectArrayDeserializer5165Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/ObjectArrayDeserializer5165Test.java new file mode 100644 index 0000000000..7e77004fb1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/ObjectArrayDeserializer5165Test.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.databind.deser.jdk; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidNullException; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// For [databind#5165] +public class ObjectArrayDeserializer5165Test +{ + static class Dst { + public Integer[] array; + } + + @Test + public void nullsFailTest() { + ObjectMapper mapper = JsonMapper.builder() + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.FAIL)) + .build(); + + // NOTE! Relies on default coercion of "" into `null` for `Integer`s... + assertThrows( + InvalidNullException.class, + () -> mapper.readValue("{\"array\":[\"\"]}", Dst.class) + ); + } + + @Test + public void nullsSkipTest() throws Exception { + ObjectMapper mapper = JsonMapper.builder() + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)) + .build(); + + Dst dst = mapper.readValue("{\"array\":[\"\"]}", Dst.class); + // NOTE! Relies on default coercion of "" into `null` for `Integer`s... + assertEquals(0, dst.array.length, "Null values should be skipped"); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringArrayDeserializer5165Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringArrayDeserializer5165Test.java new file mode 100644 index 0000000000..fd18fa68ba --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringArrayDeserializer5165Test.java @@ -0,0 +1,81 @@ +package com.fasterxml.jackson.databind.deser.jdk; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +import com.fasterxml.jackson.core.JsonParser; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidNullException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// For [databind#5165] +public class StringArrayDeserializer5165Test +{ + static class Dst { + public String[] array; + } + + // Custom deserializer that converts empty strings to null + static class EmptyStringToNullDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public EmptyStringToNullDeserializer() { + super(String.class); + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + if (value != null && value.isEmpty()) { + return null; + } + return value; + } + } + + private ObjectMapper createMapperWithCustomDeserializer() { + SimpleModule module = new SimpleModule() + .addDeserializer(String.class, new EmptyStringToNullDeserializer()); + + return JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.FAIL)) + .build(); + } + + @Test + public void nullsFailTest() { + ObjectMapper mapper = createMapperWithCustomDeserializer(); + + assertThrows( + InvalidNullException.class, + () -> mapper.readValue("{\"array\":[\"\"]}", Dst.class) + ); + } + + @Test + public void nullsSkipTest() throws Exception { + SimpleModule module = new SimpleModule() + .addDeserializer(String.class, new EmptyStringToNullDeserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)) + .build(); + + Dst dst = mapper.readValue("{\"array\":[\"\"]}", Dst.class); + + assertEquals(0, dst.array.length, "Null values should be skipped"); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringCollectionDeserializer5165Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringCollectionDeserializer5165Test.java new file mode 100644 index 0000000000..11cbf28229 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/StringCollectionDeserializer5165Test.java @@ -0,0 +1,80 @@ +package com.fasterxml.jackson.databind.deser.jdk; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidNullException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// For [databind#5165] +public class StringCollectionDeserializer5165Test +{ + static class Dst { + public List list; + } + + // Custom deserializer that converts empty strings to null + static class EmptyStringToNullDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public EmptyStringToNullDeserializer() { + super(String.class); + } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + if (value != null && value.isEmpty()) { + return null; + } + return value; + } + } + + private ObjectMapper createMapperWithCustomDeserializer() { + SimpleModule module = new SimpleModule() + .addDeserializer(String.class, new EmptyStringToNullDeserializer()); + + return JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.FAIL)) + .build(); + } + + @Test + public void nullsFailTest() { + ObjectMapper mapper = createMapperWithCustomDeserializer(); + + assertThrows( + InvalidNullException.class, + () -> mapper.readValue("{\"list\":[\"\"]}", Dst.class) + ); + } + + @Test + public void nullsSkipTest() throws Exception { + SimpleModule module = new SimpleModule() + .addDeserializer(String.class, new EmptyStringToNullDeserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)) + .build(); + + Dst dst = mapper.readValue("{\"list\":[\"\"]}", Dst.class); + + assertTrue(dst.list.isEmpty(), "Null values should be skipped"); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/EnumSetDeserializer5165Test.java b/src/test/java/com/fasterxml/jackson/databind/tofix/EnumSetDeserializer5165Test.java new file mode 100644 index 0000000000..49542266a6 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/tofix/EnumSetDeserializer5165Test.java @@ -0,0 +1,96 @@ +package com.fasterxml.jackson.databind.tofix; + +import java.io.IOException; +import java.util.EnumSet; + +import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidNullException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// For [databind#5165] +public class EnumSetDeserializer5165Test +{ + public enum MyEnum { + FOO + } + + static class Dst { + private EnumSet set; + + public EnumSet getSet() { + return set; + } + + public void setSet(EnumSet set) { + this.set = set; + } + } + + // Custom deserializer that converts empty strings to null + static class EmptyStringToNullDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public EmptyStringToNullDeserializer() { + super(MyEnum.class); + } + + @Override + public MyEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + if (value != null && value.isEmpty()) { + return null; + } + return MyEnum.valueOf(value); + } + } + + private ObjectMapper createMapperWithCustomDeserializer() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(MyEnum.class, new EmptyStringToNullDeserializer()); + + return JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.FAIL)) + .build(); + } + + @JacksonTestFailureExpected + @Test + public void nullsFailTest() { + ObjectMapper mapper = createMapperWithCustomDeserializer(); + + assertThrows( + InvalidNullException.class, + () -> mapper.readValue("{\"set\":[\"\"]}", new TypeReference(){}) + ); + } + + @JacksonTestFailureExpected + @Test + public void nullsSkipTest() throws Exception { + SimpleModule module = new SimpleModule(); + module.addDeserializer(MyEnum.class, new EmptyStringToNullDeserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModule(module) + .defaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)) + .build(); + + Dst dst = mapper.readValue("{\"set\":[\"FOO\",\"\"]}", new TypeReference() {}); + + assertTrue(dst.getSet().isEmpty(), "Null values should be skipped"); + } +}