From 97c123630cba74e4cf82e5264d30271fc9c01420 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:18:30 +0900 Subject: [PATCH 1/6] Fix #6065: APPLY_JSON_INCLUDE_FOR_CONTAINERS does not fully remove empty collection Content-level `@JsonInclude` filtering (added for containers in #5369) suppresses empty/null elements at serialization time, but the collection serializers' `isEmpty()` only checked the raw `Collection.isEmpty()`. So a collection that becomes empty *after* content filtering (e.g. a `List` holding only nulls under `content = NON_EMPTY`) was still emitted as `[]` and not dropped by value-level `NON_EMPTY`, whereas the equivalent `Map` was correctly dropped (#1649). Make `isEmpty()` content-aware for `CollectionSerializer`, `IndexedListSerializer` and `StaticListSerializerBase`, mirroring `MapSerializer`: when `APPLY_JSON_INCLUDE_FOR_CONTAINERS` is enabled and content suppression is configured, a container whose every element would be suppressed is reported as empty. Gated on `_needToCheckFiltering`, so default behavior is unchanged. --- release-notes/VERSION | 4 + .../ser/jdk/CollectionSerializer.java | 10 +- .../ser/jdk/IndexedListSerializer.java | 11 +- .../ser/jdk/StaticListSerializerBase.java | 26 +++- .../ser/std/AsArraySerializerBase.java | 46 +++++++ .../JsonIncludeContainerEmpty6065Test.java | 113 ++++++++++++++++++ 6 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java diff --git a/release-notes/VERSION b/release-notes/VERSION index dc0572ae76..6eebd9411a 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -7,6 +7,10 @@ Versions: 3.x (for earlier see VERSION-2.x) 3.3.0 (not yet released) +#6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully + remove empty collection during serialization + (reported by @doeiqts) + (fix by @seonwooj0810) #1127: `@JsonTypeInfo` with `EXTERNAL_PROPERTY` does not handle arrays of polymorphic types: now fails eagerly with clear `InvalidDefinitionException` (on both serialization and deserialization) instead of confusing low-level error diff --git a/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java index 3dd6566351..3d5628bb3b 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java @@ -86,7 +86,15 @@ protected CollectionSerializer withResolved(BeanProperty property, @Override public boolean isEmpty(SerializationContext prov, Collection value) { - return value.isEmpty(); + if (value.isEmpty()) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // a collection whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + return _allElementsSuppressed(prov, value.iterator()); + } + return false; } @Override diff --git a/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java index d3a7a94835..6639f70859 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java @@ -66,7 +66,16 @@ public IndexedListSerializer withResolved(BeanProperty property, @Override public boolean isEmpty(SerializationContext prov, Object value) { - return ((List)value).isEmpty(); + List list = (List) value; + if (list.isEmpty()) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // a list whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + return _allElementsSuppressed(prov, list.iterator()); + } + return false; } @Override diff --git a/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java b/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java index 8648c06ec1..8ab12dc3b6 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java @@ -199,7 +199,31 @@ public ValueSerializer createContextual(SerializationContext ctxt, @Override public boolean isEmpty(SerializationContext provider, T value) { - return (value == null) || (value.isEmpty()); + if ((value == null) || value.isEmpty()) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // a collection whose every element is suppressed is considered empty + if (_needToCheckFiltering(provider)) { + for (Object elem : value) { + if (elem == null) { + if (_suppressNulls) { + continue; + } + return false; + } + if (_suppressableValue == MARKER_FOR_EMPTY) { + // elements are "natural" types (Strings); check emptiness directly + if (!(elem instanceof String str) || !str.isEmpty()) { + return false; + } + } else if ((_suppressableValue == null) || !_suppressableValue.equals(elem)) { + return false; + } + } + return true; + } + return false; } @Override diff --git a/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java b/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java index 64d2ae8ab3..2cbfec98cd 100644 --- a/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java @@ -1,5 +1,6 @@ package tools.jackson.databind.ser.std; +import java.util.Iterator; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; @@ -424,4 +425,49 @@ protected boolean _shouldSerializeElement(SerializationContext ctxt, } return !_suppressableValue.equals(elem); } + + /** + * Helper method for content-aware emptiness checks (used by {@code isEmpty}): + * when content {@code @JsonInclude} is applied to containers, a container is + * considered empty if every one of its elements would be suppressed during + * serialization (so the container would render as an empty array). Callers + * should only invoke this when {@link #_needToCheckFiltering} returns true. + * + * @since 3.3.0 + */ + protected boolean _allElementsSuppressed(SerializationContext ctxt, Iterator it) + { + var serializers = _dynamicValueSerializers; + while (it.hasNext()) { + Object elem = it.next(); + if (elem == null) { + if (_suppressNulls) { + continue; + } + return false; + } + if (_suppressableValue == MARKER_FOR_EMPTY) { + ValueSerializer serializer = _elementSerializer; + if (serializer == null) { + Class cc = elem.getClass(); + serializer = serializers.serializerFor(cc); + if (serializer == null) { + if (_elementType.hasGenericTypes()) { + serializer = _findAndAddDynamic(ctxt, + ctxt.constructSpecializedType(_elementType, cc)); + } else { + serializer = _findAndAddDynamic(ctxt, cc); + } + serializers = _dynamicValueSerializers; + } + } + if (!serializer.isEmpty(ctxt, elem)) { + return false; + } + } else if ((_suppressableValue == null) || !_suppressableValue.equals(elem)) { + return false; + } + } + return true; + } } diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java new file mode 100644 index 0000000000..b37a3d3a3e --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java @@ -0,0 +1,113 @@ +package tools.jackson.databind.ser.filter; + +import java.util.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [databind#6065]: SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS should +// drop containers that become empty once content @JsonInclude filtering is applied, +// matching the behavior already implemented for Maps (#1649). +public class JsonIncludeContainerEmpty6065Test extends DatabindTestUtil +{ + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class Bean { + public String myString; + public List myList; + public Map myMap; + } + + // List of non-String elements (routed through IndexedListSerializer) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class IntListBean { + public List values; + } + + // Set (routed through CollectionSerializer) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class StringSetBean { + public Set values; + } + + private final ObjectMapper MAPPER = JsonMapper.builder() + .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS) + .build(); + + private final ObjectMapper NO_FEATURE = JsonMapper.builder() + .disable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS) + .build(); + + @Test + public void testAllNull() throws Exception { + assertEquals("{}", MAPPER.writeValueAsString(new Bean())); + } + + @Test + public void testEmptyContainers() throws Exception { + Bean bean = new Bean(); + bean.myList = new ArrayList<>(); + bean.myMap = new HashMap<>(); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testContainersEmptyAfterContentFilter() throws Exception { + Bean bean = new Bean(); + bean.myList = new ArrayList<>(Collections.singletonList(null)); + bean.myMap = new HashMap<>(); + bean.myMap.put("1", null); + // Both list and map become empty once null content is suppressed + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testStringListWithEmptyStringOnly() throws Exception { + Bean bean = new Bean(); + bean.myList = new ArrayList<>(Arrays.asList("", "")); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testStringListKeepsNonEmpty() throws Exception { + Bean bean = new Bean(); + bean.myList = new ArrayList<>(Arrays.asList(null, "keep", "")); + assertEquals("{\"myList\":[\"keep\"]}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testIntListWithNullsOnly() throws Exception { + IntListBean bean = new IntListBean(); + bean.values = new ArrayList<>(Arrays.asList(null, null)); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testIntListKeepsValues() throws Exception { + IntListBean bean = new IntListBean(); + bean.values = new ArrayList<>(Arrays.asList(null, 42)); + assertEquals("{\"values\":[42]}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testStringSetWithNullsOnly() throws Exception { + StringSetBean bean = new StringSetBean(); + bean.values = new LinkedHashSet<>(Arrays.asList((String) null)); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + // Without the feature, containers are left intact (only null containers dropped) + @Test + public void testFeatureDisabledLeavesContainers() throws Exception { + Bean bean = new Bean(); + bean.myList = new ArrayList<>(Collections.singletonList(null)); + assertEquals("{\"myList\":[null]}", NO_FEATURE.writeValueAsString(bean)); + } +} From b8f38395cc58d144e7ed1165868c4d434a13c793 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 5 Jul 2026 17:44:20 -0700 Subject: [PATCH 2/6] Extend fix to remaing collection types --- .../databind/ser/jdk/IterableSerializer.java | 12 +++- .../ser/jdk/ObjectArraySerializer.java | 48 ++++++++++++- .../ser/jdk/StringArraySerializer.java | 21 +++++- .../JsonIncludeContainerEmpty6065Test.java | 70 +++++++++++++++++++ 4 files changed, 147 insertions(+), 4 deletions(-) diff --git a/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java index d7914b20db..5401ecc922 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java @@ -55,8 +55,16 @@ public IterableSerializer withResolved(BeanProperty property, @Override public boolean isEmpty(SerializationContext ctxt, Iterable value) { - // Not really good way to implement this, but has to do for now: - return !value.iterator().hasNext(); + Iterator it = value.iterator(); + if (!it.hasNext()) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an Iterable whose every element is suppressed is considered empty + if (_needToCheckFiltering(ctxt)) { + return _allElementsSuppressed(ctxt, it); + } + return false; } @Override diff --git a/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java index f0c9b7a756..55fcb664be 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java @@ -241,7 +241,15 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, Object[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + return _allElementsSuppressed(prov, value); + } + return false; } @Override @@ -480,6 +488,44 @@ protected boolean _shouldSerializeElement(SerializationContext ctxt, return !_suppressableValue.equals(elem); } + /** + * Helper method for content-aware emptiness checks (used by {@link #isEmpty}): + * when content {@code @JsonInclude} is applied to containers, an array is + * considered empty if every one of its elements would be suppressed during + * serialization (so the array would render as an empty array). Callers should + * only invoke this when {@link #_needToCheckFiltering} returns true. + * + * @since 3.3.0 + */ + protected boolean _allElementsSuppressed(SerializationContext ctxt, Object[] value) + { + for (Object elem : value) { + if (elem == null) { + if (_suppressNulls) { + continue; + } + return false; + } + ValueSerializer serializer = _elementSerializer; + if (serializer == null) { + Class cc = elem.getClass(); + serializer = _dynamicValueSerializers.serializerFor(cc); + if (serializer == null) { + if (_elementType.hasGenericTypes()) { + serializer = _findAndAddDynamic(ctxt, + ctxt.constructSpecializedType(_elementType, cc)); + } else { + serializer = _findAndAddDynamic(ctxt, cc); + } + } + } + if (_shouldSerializeElement(ctxt, elem, serializer)) { + return false; + } + } + return true; + } + @Override public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) { diff --git a/src/main/java/tools/jackson/databind/ser/jdk/StringArraySerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/StringArraySerializer.java index fe7a83bac0..259e25fb5e 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/StringArraySerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/StringArraySerializer.java @@ -185,7 +185,26 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, String[] value) { - return (value.length == 0); + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (String str : value) { + if (str == null) { + if (_suppressNulls) { + continue; + } + return false; + } + if (_shouldSerializeElement(prov, str, _elementSerializer)) { + return false; + } + } + return true; + } + return false; } @Override diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java index b37a3d3a3e..994202fcb3 100644 --- a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java @@ -37,6 +37,31 @@ static class StringSetBean { public Set values; } + // String array (routed through StringArraySerializer) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class StringArrayBean { + public String[] values; + } + + // Object array (routed through ObjectArraySerializer) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class IntArrayBean { + public Integer[] values; + } + + // Iterable (routed through IterableSerializer): must be a genuine non-Collection + // Iterable, otherwise runtime-type resolution routes it through CollectionSerializer + static class StringIterable implements Iterable { + private final List _values; + StringIterable(String... values) { _values = Arrays.asList(values); } + @Override public Iterator iterator() { return _values.iterator(); } + } + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class IterableBean { + public Iterable values; + } + private final ObjectMapper MAPPER = JsonMapper.builder() .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS) .build(); @@ -103,6 +128,51 @@ public void testStringSetWithNullsOnly() throws Exception { assertEquals("{}", MAPPER.writeValueAsString(bean)); } + // [databind#6065]: same handling for String arrays + @Test + public void testStringArrayEmptyAfterContentFilter() throws Exception { + StringArrayBean bean = new StringArrayBean(); + bean.values = new String[] { null, "" }; + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testStringArrayKeepsNonEmpty() throws Exception { + StringArrayBean bean = new StringArrayBean(); + bean.values = new String[] { null, "keep", "" }; + assertEquals("{\"values\":[\"keep\"]}", MAPPER.writeValueAsString(bean)); + } + + // [databind#6065]: same handling for Object arrays + @Test + public void testObjectArrayNullsOnly() throws Exception { + IntArrayBean bean = new IntArrayBean(); + bean.values = new Integer[] { null, null }; + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testObjectArrayKeepsValues() throws Exception { + IntArrayBean bean = new IntArrayBean(); + bean.values = new Integer[] { null, 42 }; + assertEquals("{\"values\":[42]}", MAPPER.writeValueAsString(bean)); + } + + // [databind#6065]: same handling for Iterable + @Test + public void testIterableEmptyAfterContentFilter() throws Exception { + IterableBean bean = new IterableBean(); + bean.values = new StringIterable(null, ""); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testIterableKeepsNonEmpty() throws Exception { + IterableBean bean = new IterableBean(); + bean.values = new StringIterable(null, "keep", ""); + assertEquals("{\"values\":[\"keep\"]}", MAPPER.writeValueAsString(bean)); + } + // Without the feature, containers are left intact (only null containers dropped) @Test public void testFeatureDisabledLeavesContainers() throws Exception { From c94a2d3b42488cb0556b868bc95e5775b79cf6cd Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 5 Jul 2026 18:46:30 -0700 Subject: [PATCH 3/6] Further extend the fix --- .../databind/ser/jdk/EnumSetSerializer.java | 10 ++- .../databind/ser/jdk/JDKArraySerializers.java | 90 +++++++++++++++++-- .../JsonIncludeContainerEmpty6065Test.java | 68 ++++++++++++++ 3 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java index 6a1463843d..ac7e3bd334 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java @@ -46,7 +46,15 @@ public EnumSetSerializer withResolved(BeanProperty property, @Override public boolean isEmpty(SerializationContext prov, EnumSet> value) { - return value.isEmpty(); + if (value.isEmpty()) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an EnumSet whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + return _allElementsSuppressed(prov, value.iterator()); + } + return false; } @Override diff --git a/src/main/java/tools/jackson/databind/ser/jdk/JDKArraySerializers.java b/src/main/java/tools/jackson/databind/ser/jdk/JDKArraySerializers.java index 395574d2a8..17ec9dac05 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/JDKArraySerializers.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/JDKArraySerializers.java @@ -150,7 +150,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, boolean[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (boolean v : value) { + if (_shouldSerializeElement(prov, Boolean.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override @@ -226,7 +239,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, short[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (short v : value) { + if (_shouldSerializeElement(prov, Short.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override @@ -378,7 +404,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, int[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (int v : value) { + if (_shouldSerializeElement(prov, Integer.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override @@ -460,7 +499,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, long[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (long v : value) { + if (_shouldSerializeElement(prov, Long.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override @@ -547,7 +599,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, float[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (float v : value) { + if (_shouldSerializeElement(prov, Float.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override @@ -649,7 +714,20 @@ public ValueSerializer getContentSerializer() { @Override public boolean isEmpty(SerializationContext prov, double[] value) { - return value.length == 0; + if (value.length == 0) { + return true; + } + // [databind#6065]: with content @JsonInclude applied to containers, + // an array whose every element is suppressed is considered empty + if (_needToCheckFiltering(prov)) { + for (double v : value) { + if (_shouldSerializeElement(prov, Double.valueOf(v))) { + return false; + } + } + return true; + } + return false; } @Override diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java index 994202fcb3..270f4fcf59 100644 --- a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java @@ -57,6 +57,35 @@ static class StringIterable implements Iterable { @Override public Iterator iterator() { return _values.iterator(); } } + // Primitive arrays (routed through JDKArraySerializers.*ArraySerializer): content + // NON_DEFAULT suppresses default-valued elements (0 / false), which is the only + // content inclusion that can suppress primitives (they are never "empty"/null). + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_DEFAULT) + static class PrimArraysBean { + public int[] ints; + public long[] longs; + public short[] shorts; + public double[] doubles; + public float[] floats; + public boolean[] bools; + } + + // EnumSet (routed through EnumSetSerializer): enums are never null/empty, so only a + // CUSTOM content filter can suppress elements. + enum Size { SMALL, LARGE } + + // Custom @JsonInclude content filter: its equals() returns true for values to suppress + static class SuppressSmallFilter { + @Override public boolean equals(Object other) { return other == Size.SMALL; } + @Override public int hashCode() { return 0; } + } + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, + content = JsonInclude.Include.CUSTOM, contentFilter = SuppressSmallFilter.class) + static class EnumSetBean { + public EnumSet values; + } + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) static class IterableBean { public Iterable values; @@ -173,6 +202,45 @@ public void testIterableKeepsNonEmpty() throws Exception { assertEquals("{\"values\":[\"keep\"]}", MAPPER.writeValueAsString(bean)); } + // [databind#6065]: primitive arrays, all-default content suppressed -> property dropped + @Test + public void testPrimitiveArraysAllDefault() throws Exception { + PrimArraysBean bean = new PrimArraysBean(); + bean.ints = new int[] { 0, 0 }; + bean.longs = new long[] { 0L }; + bean.shorts = new short[] { 0, 0 }; + bean.doubles = new double[] { 0.0, 0.0 }; + bean.floats = new float[] { 0.0f }; + bean.bools = new boolean[] { false, false }; + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testPrimitiveArraysKeepNonDefault() throws Exception { + PrimArraysBean bean = new PrimArraysBean(); + bean.ints = new int[] { 0, 42 }; + bean.longs = new long[] { 0L, 7L }; + bean.doubles = new double[] { 0.0, 1.5 }; + bean.bools = new boolean[] { false, true }; + assertEquals("{\"bools\":[true],\"doubles\":[1.5],\"ints\":[42],\"longs\":[7]}", + MAPPER.writeValueAsString(bean)); + } + + // [databind#6065]: EnumSet, all elements suppressed by CUSTOM filter -> property dropped + @Test + public void testEnumSetAllSuppressed() throws Exception { + EnumSetBean bean = new EnumSetBean(); + bean.values = EnumSet.of(Size.SMALL); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testEnumSetKeepsNonSuppressed() throws Exception { + EnumSetBean bean = new EnumSetBean(); + bean.values = EnumSet.of(Size.SMALL, Size.LARGE); + assertEquals("{\"values\":[\"LARGE\"]}", MAPPER.writeValueAsString(bean)); + } + // Without the feature, containers are left intact (only null containers dropped) @Test public void testFeatureDisabledLeavesContainers() throws Exception { From 759f4bd93917ba8dc2aeb7378a475897a977d5ad Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 9 Jul 2026 21:18:35 -0700 Subject: [PATCH 4/6] More fixes --- .../ser/jdk/ObjectArraySerializer.java | 23 +++-- .../ser/jdk/StaticListSerializerBase.java | 13 +-- .../ser/std/AsArraySerializerBase.java | 15 ++-- .../JsonIncludeContainerEmpty6065Test.java | 84 +++++++++++++++++++ 4 files changed, 109 insertions(+), 26 deletions(-) diff --git a/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java index 55fcb664be..c8c6460fd0 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/ObjectArraySerializer.java @@ -506,16 +506,21 @@ protected boolean _allElementsSuppressed(SerializationContext ctxt, Object[] val } return false; } - ValueSerializer serializer = _elementSerializer; - if (serializer == null) { - Class cc = elem.getClass(); - serializer = _dynamicValueSerializers.serializerFor(cc); + // Only the "empty" check needs an element serializer; other suppression + // mechanisms compare the element directly, so avoid resolving one + ValueSerializer serializer = null; + if (_suppressableValue == MARKER_FOR_EMPTY) { + serializer = _elementSerializer; if (serializer == null) { - if (_elementType.hasGenericTypes()) { - serializer = _findAndAddDynamic(ctxt, - ctxt.constructSpecializedType(_elementType, cc)); - } else { - serializer = _findAndAddDynamic(ctxt, cc); + Class cc = elem.getClass(); + serializer = _dynamicValueSerializers.serializerFor(cc); + if (serializer == null) { + if (_elementType.hasGenericTypes()) { + serializer = _findAndAddDynamic(ctxt, + ctxt.constructSpecializedType(_elementType, cc)); + } else { + serializer = _findAndAddDynamic(ctxt, cc); + } } } } diff --git a/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java b/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java index 8ab12dc3b6..ecd74eded9 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java @@ -207,17 +207,12 @@ public boolean isEmpty(SerializationContext provider, T value) { if (_needToCheckFiltering(provider)) { for (Object elem : value) { if (elem == null) { - if (_suppressNulls) { - continue; - } - return false; - } - if (_suppressableValue == MARKER_FOR_EMPTY) { - // elements are "natural" types (Strings); check emptiness directly - if (!(elem instanceof String str) || !str.isEmpty()) { + if (!_suppressNulls) { return false; } - } else if ((_suppressableValue == null) || !_suppressableValue.equals(elem)) { + // Elements are "natural" types (Strings), never have content serializer + // (see `createContextual()`), so pass `null`, same as `serializeContents()` + } else if (_shouldSerializeElement(elem, null, provider)) { return false; } } diff --git a/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java b/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java index 2cbfec98cd..69f38b606c 100644 --- a/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java @@ -437,7 +437,6 @@ protected boolean _shouldSerializeElement(SerializationContext ctxt, */ protected boolean _allElementsSuppressed(SerializationContext ctxt, Iterator it) { - var serializers = _dynamicValueSerializers; while (it.hasNext()) { Object elem = it.next(); if (elem == null) { @@ -446,11 +445,14 @@ protected boolean _allElementsSuppressed(SerializationContext ctxt, Iterator } return false; } + // Only the "empty" check needs an element serializer; other suppression + // mechanisms compare the element directly, so avoid resolving one + ValueSerializer serializer = null; if (_suppressableValue == MARKER_FOR_EMPTY) { - ValueSerializer serializer = _elementSerializer; + serializer = _elementSerializer; if (serializer == null) { Class cc = elem.getClass(); - serializer = serializers.serializerFor(cc); + serializer = _dynamicValueSerializers.serializerFor(cc); if (serializer == null) { if (_elementType.hasGenericTypes()) { serializer = _findAndAddDynamic(ctxt, @@ -458,13 +460,10 @@ protected boolean _allElementsSuppressed(SerializationContext ctxt, Iterator } else { serializer = _findAndAddDynamic(ctxt, cc); } - serializers = _dynamicValueSerializers; } } - if (!serializer.isEmpty(ctxt, elem)) { - return false; - } - } else if ((_suppressableValue == null) || !_suppressableValue.equals(elem)) { + } + if (_shouldSerializeElement(ctxt, elem, serializer)) { return false; } } diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java index 270f4fcf59..a056720c7c 100644 --- a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java @@ -91,6 +91,27 @@ static class IterableBean { public Iterable values; } + // Nested containers: content filtering recurses, so an inner container that becomes + // empty is itself suppressed as an element of the outer container + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class NestedListBean { + public List> values; + } + + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY) + static class MapOfListsBean { + public Map> values; + } + + // content = NON_NULL: only nulls are suppressed, empty Strings are retained + @JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_NULL) + static class NonNullContentBean { + public List strings; + public List numbers; + public String[] stringArray; + public Integer[] numberArray; + } + private final ObjectMapper MAPPER = JsonMapper.builder() .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS) .build(); @@ -241,6 +262,69 @@ public void testEnumSetKeepsNonSuppressed() throws Exception { assertEquals("{\"values\":[\"LARGE\"]}", MAPPER.writeValueAsString(bean)); } + // [databind#6065]: nested containers -- inner List that is empty after content + // filtering is itself suppressed as an element of the outer List + @Test + public void testNestedListsAllEmptyAfterContentFilter() throws Exception { + NestedListBean bean = new NestedListBean(); + bean.values = new ArrayList<>(Arrays.asList( + new ArrayList<>(Arrays.asList("", "")), + new ArrayList<>(Collections.singletonList((String) null)), + new ArrayList<>())); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testNestedListsKeepNonEmpty() throws Exception { + NestedListBean bean = new NestedListBean(); + bean.values = new ArrayList<>(Arrays.asList( + new ArrayList<>(Arrays.asList("", "")), + new ArrayList<>(Arrays.asList(null, "keep")))); + assertEquals("{\"values\":[[\"keep\"]]}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testMapOfListsAllEmptyAfterContentFilter() throws Exception { + MapOfListsBean bean = new MapOfListsBean(); + bean.values = new LinkedHashMap<>(); + bean.values.put("a", new ArrayList<>(Arrays.asList("", ""))); + bean.values.put("b", new ArrayList<>()); + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testMapOfListsKeepsNonEmpty() throws Exception { + MapOfListsBean bean = new MapOfListsBean(); + bean.values = new LinkedHashMap<>(); + bean.values.put("a", new ArrayList<>(Collections.singletonList(""))); + bean.values.put("b", new ArrayList<>(Arrays.asList(null, "keep"))); + assertEquals("{\"values\":{\"b\":[\"keep\"]}}", MAPPER.writeValueAsString(bean)); + } + + // [databind#6065]: content = NON_NULL suppresses nulls only; container with + // nothing but nulls becomes empty, but empty Strings/zeroes are retained + @Test + public void testNonNullContentAllNulls() throws Exception { + NonNullContentBean bean = new NonNullContentBean(); + bean.strings = new ArrayList<>(Arrays.asList(null, null)); + bean.numbers = new ArrayList<>(Collections.singletonList((Integer) null)); + bean.stringArray = new String[] { null }; + bean.numberArray = new Integer[] { null, null }; + assertEquals("{}", MAPPER.writeValueAsString(bean)); + } + + @Test + public void testNonNullContentRetainsEmptyValues() throws Exception { + NonNullContentBean bean = new NonNullContentBean(); + bean.strings = new ArrayList<>(Arrays.asList(null, "")); + bean.numbers = new ArrayList<>(Arrays.asList(null, 0)); + bean.stringArray = new String[] { null, "" }; + bean.numberArray = new Integer[] { null, 0 }; + assertEquals("{\"strings\":[\"\"],\"numbers\":[0]," + +"\"stringArray\":[\"\"],\"numberArray\":[0]}", + MAPPER.writeValueAsString(bean)); + } + // Without the feature, containers are left intact (only null containers dropped) @Test public void testFeatureDisabledLeavesContainers() throws Exception { From 9a041095c0441bd730b20a886f769f31a36f9414 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 10 Jul 2026 08:32:16 -0700 Subject: [PATCH 5/6] Minor test fix --- .../ser/filter/JsonIncludeContainerEmpty6065Test.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java index a056720c7c..dae4a417ba 100644 --- a/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeContainerEmpty6065Test.java @@ -320,8 +320,8 @@ public void testNonNullContentRetainsEmptyValues() throws Exception { bean.numbers = new ArrayList<>(Arrays.asList(null, 0)); bean.stringArray = new String[] { null, "" }; bean.numberArray = new Integer[] { null, 0 }; - assertEquals("{\"strings\":[\"\"],\"numbers\":[0]," - +"\"stringArray\":[\"\"],\"numberArray\":[0]}", + assertEquals("{\"numberArray\":[0],\"numbers\":[0]," + +"\"stringArray\":[\"\"],\"strings\":[\"\"]}", MAPPER.writeValueAsString(bean)); } From 4596183e6f28003f040385645a7b3e4922bf6579 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 10 Jul 2026 08:37:20 -0700 Subject: [PATCH 6/6] Update CREDITS --- release-notes/CREDITS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 93680db74f..5567df48f0 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -578,6 +578,14 @@ Yusuke Nojima (@ynojima) from working on `@JsonCreator` parameter (regression in 3.2.0) [3.2.1] +@seonwooj0810 + * Fixed #6043: `FAIL_ON_UNKNOWN_PROPERTIES` has no effect with `JsonFormat.Shape.ARRAY` + when using creator-based instantiation (such as `record`) + [3.2.1] + * Fixed #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully + remove empty collection during serialization + [3.3.0] + Alex Crowell (@alex-crowell) * Reported #1127: `@JsonTypeInfo` with `EXTERNAL_PROPERTY` does not handle arrays of polymorphic types