diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer.java index dc4b320569..8532f8388f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer.java @@ -32,6 +32,12 @@ public IndexedListSerializer(IndexedListSerializer src, Boolean unwrapSingle) { super(src, property, vts, valueSerializer, unwrapSingle); } + + public IndexedListSerializer(IndexedListSerializer src, + BeanProperty property, TypeSerializer vts, JsonSerializer valueSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, property, vts, valueSerializer, unwrapSingle, suppressableValue, suppressNulls); + } @Override public IndexedListSerializer withResolved(BeanProperty property, @@ -39,6 +45,13 @@ public IndexedListSerializer withResolved(BeanProperty property, Boolean unwrapSingle) { return new IndexedListSerializer(this, property, vts, elementSerializer, unwrapSingle); } + + @Override + public IndexedListSerializer withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IndexedListSerializer(this, property, vts, elementSerializer, unwrapSingle, suppressableValue, suppressNulls); + } /* /********************************************************** @@ -71,25 +84,53 @@ public final void serialize(List value, JsonGenerator gen, SerializerProvider if (((_unwrapSingle == null) && provider.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, gen, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, gen, provider); + } else { + serializeContents(value, gen, provider); + } return; } } gen.writeStartArray(value, len); - serializeContents(value, gen, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, gen, provider); + } else { + serializeContents(value, gen, provider); + } gen.writeEndArray(); } @Override public void serializeContents(List value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, false); + } + + @Override + protected void serializeFilteredContents(List value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, true); + } + + private void serializeContentsImpl(List value, JsonGenerator g, SerializerProvider provider, boolean filtered) + throws IOException { if (_elementSerializer != null) { - serializeContentsUsing(value, g, provider, _elementSerializer); + if (filtered) { + serializeFilteredContentsUsing(value, g, provider, _elementSerializer); + } else { + serializeContentsUsing(value, g, provider, _elementSerializer); + } return; } if (_valueTypeSerializer != null) { - serializeTypedContents(value, g, provider); + if (filtered) { + serializeFilteredTypedContents(value, g, provider); + } else { + serializeTypedContents(value, g, provider); + } return; } final int len = value.size(); @@ -102,6 +143,9 @@ public void serializeContents(List value, JsonGenerator g, SerializerProvider for (; i < len; ++i) { Object elem = value.get(i); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(g); } else { Class cc = elem.getClass(); @@ -116,6 +160,10 @@ public void serializeContents(List value, JsonGenerator g, SerializerProvider } serializers = _dynamicSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, provider)) { + continue; + } serializer.serialize(elem, g, provider); } } @@ -124,9 +172,23 @@ public void serializeContents(List value, JsonGenerator g, SerializerProvider } } - public void serializeContentsUsing(List value, JsonGenerator jgen, SerializerProvider provider, + private void serializeContentsUsing(List value, JsonGenerator jgen, SerializerProvider provider, JsonSerializer ser) throws IOException + { + serializeContentsUsingImpl(value, jgen, provider, ser, false); + } + + private void serializeFilteredContentsUsing(List value, JsonGenerator jgen, SerializerProvider provider, + JsonSerializer ser) + throws IOException + { + serializeContentsUsingImpl(value, jgen, provider, ser, true); + } + + private void serializeContentsUsingImpl(List value, JsonGenerator jgen, SerializerProvider provider, + JsonSerializer ser, boolean filtered) + throws IOException { final int len = value.size(); if (len == 0) { @@ -137,11 +199,20 @@ public void serializeContentsUsing(List value, JsonGenerator jgen, Serializer Object elem = value.get(i); try { if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(jgen); - } else if (typeSer == null) { - ser.serialize(elem, jgen, provider); } else { - ser.serializeWithType(elem, jgen, provider, typeSer); + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, ser, provider)) { + continue; + } + if (typeSer == null) { + ser.serialize(elem, jgen, provider); + } else { + ser.serializeWithType(elem, jgen, provider, typeSer); + } } } catch (Exception e) { // [JACKSON-55] Need to add reference information @@ -150,7 +221,19 @@ public void serializeContentsUsing(List value, JsonGenerator jgen, Serializer } } - public void serializeTypedContents(List value, JsonGenerator jgen, SerializerProvider provider) + private void serializeTypedContents(List value, JsonGenerator jgen, SerializerProvider provider) + throws IOException + { + serializeTypedContentsImpl(value, jgen, provider, false); + } + + private void serializeFilteredTypedContents(List value, JsonGenerator jgen, SerializerProvider provider) + throws IOException + { + serializeTypedContentsImpl(value, jgen, provider, true); + } + + private void serializeTypedContentsImpl(List value, JsonGenerator jgen, SerializerProvider provider, boolean filtered) throws IOException { final int len = value.size(); @@ -164,6 +247,9 @@ public void serializeTypedContents(List value, JsonGenerator jgen, Serializer for (; i < len; ++i) { Object elem = value.get(i); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(jgen); } else { Class cc = elem.getClass(); @@ -178,6 +264,10 @@ public void serializeTypedContents(List value, JsonGenerator jgen, Serializer } serializers = _dynamicSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, provider)) { + continue; + } serializer.serializeWithType(elem, jgen, provider, typeSer); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedStringListSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedStringListSerializer.java index 48f5d91fec..671e25ae41 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedStringListSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedStringListSerializer.java @@ -41,11 +41,22 @@ public IndexedStringListSerializer(IndexedStringListSerializer src, Boolean unwrapSingle) { super(src, unwrapSingle); } + + public IndexedStringListSerializer(IndexedStringListSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, unwrapSingle, suppressableValue, suppressNulls); + } @Override public JsonSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle) { return new IndexedStringListSerializer(this, unwrapSingle); } + + @Override + public JsonSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle, + Object suppressableValue, boolean suppressNulls) { + return new IndexedStringListSerializer(this, unwrapSingle, suppressableValue, suppressNulls); + } @Override protected JsonNode contentSchema() { return createSchemaNode("string", true); } @@ -69,12 +80,20 @@ public void serialize(List value, JsonGenerator g, if (((_unwrapSingle == null) && provider.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, provider, 1); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider, 1); + } else { + serializeContents(value, g, provider, 1); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, provider, len); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider, len); + } else { + serializeContents(value, g, provider, len); + } g.writeEndArray(); } @@ -92,14 +111,33 @@ public void serializeWithType(List value, JsonGenerator g, SerializerPro private final void serializeContents(List value, JsonGenerator g, SerializerProvider provider, int len) throws IOException + { + serializeContentsImpl(value, g, provider, len, false); + } + + private final void serializeFilteredContents(List value, JsonGenerator g, + SerializerProvider provider, int len) throws IOException + { + serializeContentsImpl(value, g, provider, len, true); + } + + private final void serializeContentsImpl(List value, JsonGenerator g, + SerializerProvider provider, int len, boolean filtered) throws IOException { int i = 0; try { for (; i < len; ++i) { String str = value.get(i); if (str == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(str, null, provider)) { + continue; + } g.writeString(str); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IteratorSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IteratorSerializer.java index 10aba9b654..7f09ceef2a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/IteratorSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/IteratorSerializer.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.ser.impl; import java.io.IOException; +import java.util.Collection; import java.util.Iterator; import com.fasterxml.jackson.core.JsonGenerator; @@ -25,6 +26,12 @@ public IteratorSerializer(IteratorSerializer src, super(src, property, vts, valueSerializer, unwrapSingle); } + public IteratorSerializer(IteratorSerializer src, + BeanProperty property, TypeSerializer vts, JsonSerializer valueSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, property, vts, valueSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public boolean isEmpty(SerializerProvider prov, Iterator value) { return !value.hasNext(); @@ -48,6 +55,13 @@ public IteratorSerializer withResolved(BeanProperty property, return new IteratorSerializer(this, property, vts, elementSerializer, unwrapSingle); } + @Override + public IteratorSerializer withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IteratorSerializer(this, property, vts, elementSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public final void serialize(Iterator value, JsonGenerator gen, SerializerProvider provider) throws IOException @@ -58,49 +72,97 @@ public final void serialize(Iterator value, JsonGenerator gen, provider.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { if (hasSingleElement(value)) { - serializeContents(value, gen, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, gen, provider); + } else { + serializeContents(value, gen, provider); + } return; } } */ gen.writeStartArray(value); - serializeContents(value, gen, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, gen, provider); + } else { + serializeContents(value, gen, provider); + } gen.writeEndArray(); } @Override public void serializeContents(Iterator value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, false); + } + + @Override + protected void serializeFilteredContents(Iterator value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, true); + } + + private void serializeContentsImpl(Iterator value, JsonGenerator g, + SerializerProvider provider, boolean filtered) throws IOException { if (!value.hasNext()) { return; } JsonSerializer serializer = _elementSerializer; if (serializer == null) { - _serializeDynamicContents(value, g, provider); + if (filtered) { + _serializeFilteredDynamicContents(value, g, provider); + } else { + _serializeDynamicContents(value, g, provider); + } return; } final TypeSerializer typeSer = _valueTypeSerializer; do { Object elem = value.next(); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(g); - } else if (typeSer == null) { - serializer.serialize(elem, g, provider); } else { - serializer.serializeWithType(elem, g, provider, typeSer); + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, provider)) { + continue; + } + if (typeSer == null) { + serializer.serialize(elem, g, provider); + } else { + serializer.serializeWithType(elem, g, provider, typeSer); + } } } while (value.hasNext()); } protected void _serializeDynamicContents(Iterator value, JsonGenerator g, SerializerProvider provider) throws IOException + { + _serializeDynamicContentsImpl(value, g, provider, false); + } + + protected void _serializeFilteredDynamicContents(Iterator value, JsonGenerator g, + SerializerProvider provider) throws IOException + { + _serializeDynamicContentsImpl(value, g, provider, true); + } + + private void _serializeDynamicContentsImpl(Iterator value, JsonGenerator g, + SerializerProvider provider, boolean filtered) throws IOException { final TypeSerializer typeSer = _valueTypeSerializer; PropertySerializerMap serializers = _dynamicSerializers; do { Object elem = value.next(); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNull(g); continue; } @@ -115,6 +177,10 @@ protected void _serializeDynamicContents(Iterator value, JsonGenerator g, } serializers = _dynamicSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, provider)) { + continue; + } if (typeSer == null) { serializer.serialize(elem, g, provider); } else { diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/StringCollectionSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/StringCollectionSerializer.java index be2e52eab7..c9eac28dba 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/StringCollectionSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/StringCollectionSerializer.java @@ -42,11 +42,22 @@ protected StringCollectionSerializer(StringCollectionSerializer src, super(src, unwrapSingle); } + protected StringCollectionSerializer(StringCollectionSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) + { + super(src, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public JsonSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle) { return new StringCollectionSerializer(this, unwrapSingle); } + @Override + public JsonSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new StringCollectionSerializer(this, unwrapSingle, suppressableValue, suppressNulls); + } + @Override protected JsonNode contentSchema() { return createSchemaNode("string", true); } @@ -72,12 +83,20 @@ public void serialize(Collection value, JsonGenerator g, if (((_unwrapSingle == null) && provider.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider); + } else { + serializeContents(value, g, provider); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider); + } else { + serializeContents(value, g, provider); + } g.writeEndArray(); } @@ -96,14 +115,37 @@ public void serializeWithType(Collection value, JsonGenerator g, private final void serializeContents(Collection value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, false); + } + + private final void serializeFilteredContents(Collection value, JsonGenerator g, + SerializerProvider provider) + throws IOException + { + serializeContentsImpl(value, g, provider, true); + } + + private final void serializeContentsImpl(Collection value, JsonGenerator g, + SerializerProvider provider, boolean filtered) + throws IOException { int i = 0; try { for (String str : value) { if (str == null) { + if (filtered && _suppressNulls) { + ++i; + continue; + } provider.defaultSerializeNull(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(str, null, provider)) { + ++i; + continue; + } g.writeString(str); } ++i; diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/AsArraySerializerBase.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/AsArraySerializerBase.java index a241080700..83ad450bc0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/AsArraySerializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/AsArraySerializerBase.java @@ -2,11 +2,13 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.Collection; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; - -import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.type.WritableTypeId; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; @@ -16,6 +18,8 @@ import com.fasterxml.jackson.databind.ser.ContainerSerializer; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; +import com.fasterxml.jackson.databind.util.ArrayBuilders; +import com.fasterxml.jackson.databind.util.BeanUtil; /** * Base class for serializers that will output contents as JSON @@ -27,6 +31,7 @@ public abstract class AsArraySerializerBase extends ContainerSerializer implements ContextualSerializer { + public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY; protected final JavaType _elementType; /** @@ -61,6 +66,20 @@ public abstract class AsArraySerializerBase */ protected PropertySerializerMap _dynamicSerializers; + /** + * Value that indicates suppression mechanism to use for + * content values (elements of container), if any; null + * for no filtering. + * @since 2.20 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * @since 2.20 + */ + protected final boolean _suppressNulls; + /* /********************************************************** /* Life-cycle @@ -110,6 +129,8 @@ protected AsArraySerializerBase(Class cls, JavaType elementType, boolean stat _elementSerializer = (JsonSerializer) elementSerializer; _dynamicSerializers = PropertySerializerMap.emptyForProperties(); _unwrapSingle = unwrapSingle; + _suppressableValue = null; + _suppressNulls = false; } @SuppressWarnings("unchecked") @@ -126,6 +147,28 @@ protected AsArraySerializerBase(AsArraySerializerBase src, // [databind#2181]: may not be safe to reuse, start from empty _dynamicSerializers = PropertySerializerMap.emptyForProperties(); _unwrapSingle = unwrapSingle; + _suppressableValue = src._suppressableValue; + _suppressNulls = src._suppressNulls; + } + + /** + * @since 2.20 + */ + @SuppressWarnings("unchecked") + protected AsArraySerializerBase(AsArraySerializerBase src, + BeanProperty property, TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) + { + super(src); + _elementType = src._elementType; + _staticTyping = src._staticTyping; + _valueTypeSerializer = vts; + _property = property; + _elementSerializer = (JsonSerializer) elementSerializer; + _dynamicSerializers = PropertySerializerMap.emptyForProperties(); + _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } /** @@ -153,6 +196,13 @@ public final AsArraySerializerBase withResolved(BeanProperty property, public abstract AsArraySerializerBase withResolved(BeanProperty property, TypeSerializer vts, JsonSerializer elementSerializer, Boolean unwrapSingle); + + /** + * @since 2.20 + */ + public abstract AsArraySerializerBase withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls); /* /********************************************************** @@ -207,11 +257,61 @@ public JsonSerializer createContextual(SerializerProvider serializers, } } } + + // Handle content inclusion (similar to MapSerializer lines 560-609) + JsonInclude.Value inclV = findIncludeOverrides(serializers, property, handledType()); + Object valueToSuppress = _suppressableValue; + boolean suppressNulls = _suppressNulls; + + if (inclV != null) { + JsonInclude.Include incl = inclV.getContentInclusion(); + if (incl != JsonInclude.Include.USE_DEFAULTS) { + switch (incl) { + case NON_DEFAULT: + valueToSuppress = BeanUtil.getDefaultValue(_elementType); + suppressNulls = true; + if (valueToSuppress != null) { + if (valueToSuppress.getClass().isArray()) { + valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress); + } + } + break; + case NON_ABSENT: + suppressNulls = true; + valueToSuppress = MARKER_FOR_EMPTY; + break; + case NON_EMPTY: + suppressNulls = true; + valueToSuppress = MARKER_FOR_EMPTY; + break; + case CUSTOM: + valueToSuppress = serializers.includeFilterInstance(null, inclV.getContentFilter()); + if (valueToSuppress == null) { + suppressNulls = true; + } else { + suppressNulls = serializers.includeFilterSuppressNulls(valueToSuppress); + } + break; + case NON_NULL: + valueToSuppress = null; + suppressNulls = true; + break; + case ALWAYS: + default: + valueToSuppress = null; + suppressNulls = false; + break; + } + } + } + if ((ser != _elementSerializer) || (property != _property) || (_valueTypeSerializer != typeSer) - || (!Objects.equals(_unwrapSingle, unwrapSingle))) { - return withResolved(property, typeSer, ser, unwrapSingle); + || (!Objects.equals(_unwrapSingle, unwrapSingle)) + || (!Objects.equals(valueToSuppress, _suppressableValue)) + || (suppressNulls != _suppressNulls)) { + return withResolved(property, typeSer, ser, unwrapSingle, valueToSuppress, suppressNulls); } return this; } @@ -269,6 +369,9 @@ public void serializeWithType(T value, JsonGenerator g, SerializerProvider provi protected abstract void serializeContents(T value, JsonGenerator gen, SerializerProvider provider) throws IOException; + protected abstract void serializeFilteredContents(T value, JsonGenerator g, SerializerProvider provider) + throws IOException; + /** * @deprecated Since 2.15 */ @@ -327,4 +430,34 @@ protected final JsonSerializer _findAndAddDynamic(PropertySerializerMap } return result.serializer; } + + /** + * Common utility method for checking if an element should be filtered/suppressed + * based on @JsonInclude settings. Returns {@code true} if element should be serialized, + * {@code false} if it should be skipped. + * + * @param elem Element to check for suppression + * @param serializer Serializer for the element (may be null for strings) + * @param provider Serializer provider + * @return true if element should be serialized, false if suppressed + * + * @since 2.21 + */ + protected final boolean _shouldSerializeElement(Object elem, JsonSerializer serializer, + SerializerProvider provider) throws IOException + { + if (_suppressableValue == null) { + return true; + } + if (_suppressableValue == MARKER_FOR_EMPTY) { + if (serializer != null) { + return !serializer.isEmpty(provider, elem); + } else { + // For strings and primitives, check emptiness directly + return elem instanceof String ? !((String) elem).isEmpty() : true; + } + } else { + return !_suppressableValue.equals(elem); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/CollectionSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/CollectionSerializer.java index 75202d2734..5f2de3bf6a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/CollectionSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/CollectionSerializer.java @@ -61,6 +61,13 @@ public CollectionSerializer(CollectionSerializer src, _maybeEnumSet = src._maybeEnumSet; } + public CollectionSerializer(CollectionSerializer src, + BeanProperty property, TypeSerializer vts, JsonSerializer valueSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, property, vts, valueSerializer, unwrapSingle, suppressableValue, suppressNulls); + _maybeEnumSet = src._maybeEnumSet; + } + @Override public ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return new CollectionSerializer(this, _property, vts, _elementSerializer, _unwrapSingle); @@ -73,6 +80,13 @@ public CollectionSerializer withResolved(BeanProperty property, return new CollectionSerializer(this, property, vts, elementSerializer, unwrapSingle); } + @Override + public CollectionSerializer withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new CollectionSerializer(this, property, vts, elementSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + /* /********************************************************** /* Accessors @@ -103,21 +117,44 @@ public final void serialize(Collection value, JsonGenerator g, SerializerProv if (((_unwrapSingle == null) && provider.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider); + } else { + serializeContents(value, g, provider); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, provider); + if ((_suppressableValue != null) || _suppressNulls) { + serializeFilteredContents(value, g, provider); + } else { + serializeContents(value, g, provider); + } g.writeEndArray(); } @Override public void serializeContents(Collection value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, false); + } + + @Override + protected void serializeFilteredContents(Collection value, JsonGenerator g, SerializerProvider provider) throws IOException + { + serializeContentsImpl(value, g, provider, true); + } + + private void serializeContentsImpl(Collection value, JsonGenerator g, SerializerProvider provider, boolean filtered) throws IOException { g.assignCurrentValue(value); if (_elementSerializer != null) { - serializeContentsUsing(value, g, provider, _elementSerializer); + if (filtered) { + serializeFilteredContentsUsing(value, g, provider, _elementSerializer); + } else { + serializeContentsUsing(value, g, provider, _elementSerializer); + } return; } Iterator it = value.iterator(); @@ -134,6 +171,10 @@ public void serializeContents(Collection value, JsonGenerator g, SerializerPr do { Object elem = it.next(); if (elem == null) { + if (filtered && _suppressNulls) { + ++i; + continue; + } provider.defaultSerializeNull(g); } else { Class cc = elem.getClass(); @@ -147,6 +188,11 @@ public void serializeContents(Collection value, JsonGenerator g, SerializerPr } serializers = _dynamicSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, provider)) { + ++i; + continue; + } if (typeSer == null) { serializer.serialize(elem, g, provider); } else { @@ -162,6 +208,18 @@ public void serializeContents(Collection value, JsonGenerator g, SerializerPr public void serializeContentsUsing(Collection value, JsonGenerator g, SerializerProvider provider, JsonSerializer ser) throws IOException + { + serializeContentsUsingImpl(value, g, provider, ser, false); + } + + private void serializeFilteredContentsUsing(Collection value, JsonGenerator g, SerializerProvider provider, + JsonSerializer ser) throws IOException + { + serializeContentsUsingImpl(value, g, provider, ser, true); + } + + private void serializeContentsUsingImpl(Collection value, JsonGenerator g, SerializerProvider provider, + JsonSerializer ser, boolean filtered) throws IOException { Iterator it = value.iterator(); if (it.hasNext()) { @@ -173,8 +231,17 @@ public void serializeContentsUsing(Collection value, JsonGenerator g, Seriali Object elem = it.next(); try { if (elem == null) { + if (filtered && _suppressNulls) { + ++i; + continue; + } provider.defaultSerializeNull(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, ser, provider)) { + ++i; + continue; + } if (typeSer == null) { ser.serialize(elem, g, provider); } else { @@ -188,4 +255,5 @@ public void serializeContentsUsing(Collection value, JsonGenerator g, Seriali } while (it.hasNext()); } } + } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSetSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSetSerializer.java index d2d14f84fd..e878703fa7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSetSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSetSerializer.java @@ -24,6 +24,12 @@ public EnumSetSerializer(EnumSetSerializer src, super(src, property, vts, valueSerializer, unwrapSingle); } + public EnumSetSerializer(EnumSetSerializer src, + BeanProperty property, TypeSerializer vts, JsonSerializer valueSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, property, vts, valueSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public EnumSetSerializer _withValueTypeSerializer(TypeSerializer vts) { // no typing for enum elements (always strongly typed), so don't change @@ -37,6 +43,13 @@ public EnumSetSerializer withResolved(BeanProperty property, return new EnumSetSerializer(this, property, vts, elementSerializer, unwrapSingle); } + @Override + public EnumSetSerializer withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new EnumSetSerializer(this, property, vts, elementSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public boolean isEmpty(SerializerProvider prov, EnumSet> value) { return value.isEmpty(); @@ -82,4 +95,11 @@ public void serializeContents(EnumSet> value, JsonGenerator ge enumSer.serialize(en, gen, provider); } } + + @Override + protected void serializeFilteredContents(EnumSet> value, JsonGenerator g, SerializerProvider provider) throws IOException { + // TODO: Implement, later? + serializeContents(value, g, provider); + } + } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/IterableSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/IterableSerializer.java index 4116ef603b..b83c72de5a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/IterableSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/IterableSerializer.java @@ -25,6 +25,12 @@ public IterableSerializer(IterableSerializer src, BeanProperty property, super(src, property, vts, valueSerializer, unwrapSingle); } + public IterableSerializer(IterableSerializer src, BeanProperty property, + TypeSerializer vts, JsonSerializer valueSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, property, vts, valueSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return new IterableSerializer(this, _property, vts, _elementSerializer, _unwrapSingle); @@ -37,6 +43,13 @@ public IterableSerializer withResolved(BeanProperty property, return new IterableSerializer(this, property, vts, elementSerializer, unwrapSingle); } + @Override + public IterableSerializer withResolved(BeanProperty property, + TypeSerializer vts, JsonSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IterableSerializer(this, property, vts, elementSerializer, unwrapSingle, suppressableValue, suppressNulls); + } + @Override public boolean isEmpty(SerializerProvider prov, Iterable value) { // Not really good way to implement this, but has to do for now: @@ -109,4 +122,10 @@ public void serializeContents(Iterable value, JsonGenerator jgen, } while (it.hasNext()); } } + + @Override + protected void serializeFilteredContents(Iterable value, JsonGenerator g, SerializerProvider provider) throws IOException { + // TODO: Implement, later? + serializeContents(value, g, provider); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StaticListSerializerBase.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StaticListSerializerBase.java index a641266db8..5495f9f8d2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StaticListSerializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StaticListSerializerBase.java @@ -6,6 +6,7 @@ import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.*; @@ -14,6 +15,8 @@ import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.util.ArrayBuilders; +import com.fasterxml.jackson.databind.util.BeanUtil; /** * Intermediate base class for Lists, Collections and Arrays @@ -24,6 +27,7 @@ public abstract class StaticListSerializerBase> extends StdSerializer implements ContextualSerializer { + public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY; /** * Setting for specific local override for "unwrap single element arrays": * true for enable unwrapping, false for preventing it, `null` for using @@ -32,10 +36,26 @@ public abstract class StaticListSerializerBase> * @since 2.6 */ protected final Boolean _unwrapSingle; + + /** + * Value that indicates suppression mechanism to use for + * content values (elements of container), if any; null + * for no filtering. + * @since 2.20 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * @since 2.20 + */ + protected final boolean _suppressNulls; protected StaticListSerializerBase(Class cls) { super(cls, false); _unwrapSingle = null; + _suppressableValue = null; + _suppressNulls = false; } /** @@ -45,6 +65,19 @@ protected StaticListSerializerBase(StaticListSerializerBase src, Boolean unwrapSingle) { super(src); _unwrapSingle = unwrapSingle; + _suppressableValue = src._suppressableValue; + _suppressNulls = src._suppressNulls; + } + + /** + * @since 2.20 + */ + protected StaticListSerializerBase(StaticListSerializerBase src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src); + _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } /** @@ -52,6 +85,12 @@ protected StaticListSerializerBase(StaticListSerializerBase src, */ public abstract JsonSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle); + + /** + * @since 2.20 + */ + public abstract JsonSerializer _withResolved(BeanProperty prop, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls); /* /********************************************************** @@ -87,12 +126,61 @@ public JsonSerializer createContextual(SerializerProvider serializers, if (ser == null) { ser = serializers.findContentValueSerializer(String.class, property); } + + // Handle content inclusion (similar to MapSerializer lines 560-609) + JsonInclude.Value inclV = findIncludeOverrides(serializers, property, List.class); + Object valueToSuppress = _suppressableValue; + boolean suppressNulls = _suppressNulls; + + if (inclV != null) { + JsonInclude.Include incl = inclV.getContentInclusion(); + if (incl != JsonInclude.Include.USE_DEFAULTS) { + switch (incl) { + case NON_DEFAULT: + valueToSuppress = BeanUtil.getDefaultValue(serializers.constructType(String.class)); + suppressNulls = true; + if (valueToSuppress != null) { + if (valueToSuppress.getClass().isArray()) { + valueToSuppress = ArrayBuilders.getArrayComparator(valueToSuppress); + } + } + break; + case NON_ABSENT: + suppressNulls = true; + valueToSuppress = MARKER_FOR_EMPTY; + break; + case NON_EMPTY: + suppressNulls = true; + valueToSuppress = MARKER_FOR_EMPTY; + break; + case CUSTOM: + valueToSuppress = serializers.includeFilterInstance(null, inclV.getContentFilter()); + if (valueToSuppress == null) { + suppressNulls = true; + } else { + suppressNulls = serializers.includeFilterSuppressNulls(valueToSuppress); + } + break; + case NON_NULL: + valueToSuppress = null; + suppressNulls = true; + break; + case ALWAYS: + default: + valueToSuppress = null; + suppressNulls = false; + break; + } + } + } // Optimization: default serializer just writes String, so we can avoid a call: if (isDefaultSerializer(ser)) { - if (Objects.equals(unwrapSingle, _unwrapSingle)) { + if (Objects.equals(unwrapSingle, _unwrapSingle) + && Objects.equals(valueToSuppress, _suppressableValue) + && suppressNulls == _suppressNulls) { return this; } - return _withResolved(property, unwrapSingle); + return _withResolved(property, unwrapSingle, valueToSuppress, suppressNulls); } // otherwise... // note: will never have TypeSerializer, because Strings are "natural" type @@ -137,4 +225,34 @@ protected abstract void acceptContentVisitor(JsonArrayFormatVisitor visitor) @Override public abstract void serializeWithType(T value, JsonGenerator g, SerializerProvider provider, TypeSerializer typeSer) throws IOException; + + /** + * Common utility method for checking if an element should be filtered/suppressed + * based on @JsonInclude settings. Returns {@code true} if element should be serialized, + * {@code false} if it should be skipped. + * + * @param elem Element to check for suppression + * @param serializer Serializer for the element (may be null for strings) + * @param provider Serializer provider + * @return true if element should be serialized, false if suppressed + * + * @since 2.21 + */ + protected final boolean _shouldSerializeElement(Object elem, JsonSerializer serializer, + SerializerProvider provider) throws IOException + { + if (_suppressableValue == null) { + return true; + } + if (_suppressableValue == MARKER_FOR_EMPTY) { + if (serializer != null) { + return !serializer.isEmpty(provider, elem); + } else { + // For strings and primitives, check emptiness directly + return elem instanceof String ? !((String) elem).isEmpty() : true; + } + } else { + return !_suppressableValue.equals(elem); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/filter/JsonIncludeForCollection5227Test.java b/src/test/java/com/fasterxml/jackson/databind/ser/filter/JsonIncludeForCollection5227Test.java new file mode 100644 index 0000000000..f317c7890c --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/filter/JsonIncludeForCollection5227Test.java @@ -0,0 +1,227 @@ +package com.fasterxml.jackson.databind.ser.filter; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// Tests for [databind#888] +public class JsonIncludeForCollection5227Test + extends DatabindTestUtil +{ + static class FooFilter { + @Override + public boolean equals(Object other) { + if (other == null) { // do NOT filter out nulls + return false; + } + // in fact, only filter out exact String "foo" + return "foo".equals(other); + } + } + + static class FooListBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = FooFilter.class) + public List items = new ArrayList(); + + public FooListBean add(String value) { + items.add(value); + return this; + } + } + + // Test NON_NULL content inclusion + static class NonNullListBean { + @JsonInclude(content = JsonInclude.Include.NON_NULL) + public List items = new ArrayList(); + + public NonNullListBean add(String value) { + items.add(value); + return this; + } + } + + // Test NON_EMPTY content inclusion + static class NonEmptyListBean { + @JsonInclude(content = JsonInclude.Include.NON_EMPTY) + public List items = new ArrayList(); + + public NonEmptyListBean add(String value) { + items.add(value); + return this; + } + } + + // Test NON_DEFAULT content inclusion + static class NonDefaultListBean { + @JsonInclude(content = JsonInclude.Include.NON_DEFAULT) + public List items = new ArrayList(); + + public NonDefaultListBean add(String value) { + items.add(value); + return this; + } + } + + // Test with different collection types + static class FooSetBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = FooFilter.class) + public Set items = new LinkedHashSet(); + + public FooSetBean add(String value) { + items.add(value); + return this; + } + } + + // Test with Integer values + static class NumberFilter { + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + return Integer.valueOf(42).equals(other); + } + } + + static class NumberListBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = NumberFilter.class) + public List numbers = new ArrayList(); + + public NumberListBean add(Integer value) { + numbers.add(value); + return this; + } + } + + // Test counting filter behavior + static class CountingFooFilter { + public final static AtomicInteger counter = new AtomicInteger(0); + + @Override + public boolean equals(Object other) { + counter.incrementAndGet(); + return "foo".equals(other); + } + } + + static class CountingFooListBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = CountingFooFilter.class) + public List items = new ArrayList(); + + public CountingFooListBean add(String value) { + items.add(value); + return this; + } + } + + /* + /********************************************************** + /* Test methods, success + /********************************************************** + */ + + final private ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void testCustomFilterWithList() throws Exception { + FooListBean input = new FooListBean() + .add("1") + .add("foo") + .add("2"); + + assertEquals(a2q("{'items':['1','2']}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testNonNullContentInclusion() throws Exception { + NonNullListBean input = new NonNullListBean() + .add("1") + .add(null) + .add("2"); + + assertEquals(a2q("{'items':['1','2']}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testNonEmptyContentInclusion() throws Exception { + NonEmptyListBean input = new NonEmptyListBean() + .add("1") + .add("") + .add("2"); + + assertEquals(a2q("{'items':['1','2']}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testNonDefaultContentInclusion() throws Exception { + NonDefaultListBean input = new NonDefaultListBean() + .add("1") + .add(null) // null is default for String + .add("2"); + + assertEquals(a2q("{'items':['1','2']}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithSet() throws Exception { + FooSetBean input = new FooSetBean() + .add("1") + .add("foo") + .add("2"); + + assertEquals(a2q("{'items':['1','2']}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithNumbers() throws Exception { + NumberListBean input = new NumberListBean() + .add(1) + .add(42) + .add(3); + + assertEquals(a2q("{'numbers':[1,3]}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testEmptyListWithCustomFilter() throws Exception { + FooListBean input = new FooListBean(); + assertEquals(a2q("{'items':[]}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testAllFilteredOut() throws Exception { + FooListBean input = new FooListBean() + .add("foo") + .add("foo") + .add("foo"); + + assertEquals(a2q("{'items':[]}"), MAPPER.writeValueAsString(input)); + } + + @Test + public void testMixedNullsAndFiltered() throws Exception { + FooListBean input = new FooListBean() + .add("1") + .add(null) + .add("foo") + .add("2") + .add(null); + + // Custom filter should not filter nulls (based on FooFilter.equals implementation) + assertEquals(a2q("{'items':['1',null,'2',null]}"), MAPPER.writeValueAsString(input)); + } + + +}