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 065332dead..d6af311eaa 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java @@ -47,10 +47,21 @@ public CollectionSerializer(JavaType elemType, boolean staticTyping, TypeSeriali _maybeEnumSet = elemType.isEnumType() || elemType.isJavaLangObject(); } + @Deprecated // since 3.1.0 protected CollectionSerializer(CollectionSerializer src, TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, BeanProperty property) { - super(src, vts, valueSerializer, unwrapSingle, property); + this(src, vts, valueSerializer, unwrapSingle, property, null, false); + } + + /** + * @since 3.1.0 + */ + protected CollectionSerializer(CollectionSerializer src, + TypeSerializer vts, ValueSerializer valueSerializer, + Boolean unwrapSingle, BeanProperty property, + Object suppressableValue, boolean suppressNulls) { + super(src, vts, valueSerializer, unwrapSingle, property, suppressableValue, suppressNulls); _maybeEnumSet = src._maybeEnumSet; } @@ -63,7 +74,15 @@ protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) protected CollectionSerializer withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, Boolean unwrapSingle) { - return new CollectionSerializer(this, vts, elementSerializer, unwrapSingle, property); + return withResolved(property, vts, elementSerializer, unwrapSingle, null, false); + } + + // @since 3.1.0 + @Override + protected CollectionSerializer withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new CollectionSerializer(this, vts, elementSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } /* @@ -97,21 +116,43 @@ public final void serialize(Collection value, JsonGenerator g, SerializationC 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, SerializationContext ctxt) - throws JacksonException + { + serializeContentsImpl(value, g, ctxt, false); + } + + @Override + protected void serializeFilteredContents(Collection value, JsonGenerator g, SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, true); + } + + private void serializeContentsImpl(Collection value, JsonGenerator g, SerializationContext ctxt, boolean filtered) throws JacksonException { if (_elementSerializer != null) { - serializeContentsUsing(value, g, ctxt, _elementSerializer); + if (filtered) { + serializeFilteredContentsUsing(value, g, ctxt, _elementSerializer); + } else { + serializeContentsUsing(value, g, ctxt, _elementSerializer); + } return; } Iterator it = value.iterator(); @@ -128,6 +169,10 @@ public void serializeContents(Collection value, JsonGenerator g, Serializatio do { Object elem = it.next(); if (elem == null) { + if (filtered && _suppressNulls) { + ++i; + continue; + } ctxt.defaultSerializeNullValue(g); } else { Class cc = elem.getClass(); @@ -140,6 +185,11 @@ public void serializeContents(Collection value, JsonGenerator g, Serializatio } serializers = _dynamicValueSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + ++i; + continue; + } if (typeSer == null) { serializer.serialize(elem, g, ctxt); } else { @@ -153,9 +203,21 @@ public void serializeContents(Collection value, JsonGenerator g, Serializatio } } - public void serializeContentsUsing(Collection value, JsonGenerator g, SerializationContext provider, + public void serializeContentsUsing(Collection value, JsonGenerator g, SerializationContext ctxt, ValueSerializer ser) throws JacksonException + { + serializeContentsUsingImpl(value, g, ctxt, ser, false); + } + + private void serializeFilteredContentsUsing(Collection value, JsonGenerator g, SerializationContext ctxt, + ValueSerializer ser) throws JacksonException + { + serializeContentsUsingImpl(value, g, ctxt, ser, true); + } + + private void serializeContentsUsingImpl(Collection value, JsonGenerator g, SerializationContext ctxt, + ValueSerializer ser, boolean filtered) throws JacksonException { Iterator it = value.iterator(); if (it.hasNext()) { @@ -167,17 +229,26 @@ public void serializeContentsUsing(Collection value, JsonGenerator g, Seriali Object elem = it.next(); try { if (elem == null) { - provider.defaultSerializeNullValue(g); + if (filtered && _suppressNulls) { + ++i; + continue; + } + ctxt.defaultSerializeNullValue(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, ser, ctxt)) { + ++i; + continue; + } if (typeSer == null) { - ser.serialize(elem, g, provider); + ser.serialize(elem, g, ctxt); } else { - ser.serializeWithType(elem, g, provider, typeSer); + ser.serializeWithType(elem, g, ctxt, typeSer); } } ++i; } catch (Exception e) { - wrapAndThrow(provider, e, value, i); + wrapAndThrow(ctxt, e, value, i); } } while (it.hasNext()); } 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 4c2abcf8cf..159c05d553 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/EnumSetSerializer.java @@ -14,10 +14,21 @@ public EnumSetSerializer(JavaType elemType) { super(EnumSet.class, elemType, true, null, null); } + @Deprecated // since 3.1.0 public EnumSetSerializer(EnumSetSerializer src, TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, BeanProperty property) { - super(src, vts, valueSerializer, unwrapSingle, property); + this(src, vts, valueSerializer, unwrapSingle, property, null, false); + } + + /** + * @since 3.1.0 + */ + public EnumSetSerializer(EnumSetSerializer src, + TypeSerializer vts, ValueSerializer valueSerializer, + Boolean unwrapSingle, BeanProperty property, + Object suppressableValue, boolean suppressNulls) { + super(src, vts, valueSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } @Override @@ -30,7 +41,14 @@ protected EnumSetSerializer _withValueTypeSerializer(TypeSerializer vts) { protected EnumSetSerializer withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, Boolean unwrapSingle) { - return new EnumSetSerializer(this, vts, elementSerializer, unwrapSingle, property); + return new EnumSetSerializer(this, vts, elementSerializer, unwrapSingle, property, null, false); + } + + @Override + public EnumSetSerializer withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new EnumSetSerializer(this, vts, elementSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } @Override @@ -80,4 +98,5 @@ public void serializeContents(EnumSet> value, JsonGenerator ge enumSer.serialize(en, gen, ctxt); } } + } 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 2326acba28..23aa47a8dd 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java @@ -26,10 +26,20 @@ public IndexedListSerializer(JavaType elemType, boolean staticTyping, TypeSerial super(List.class, elemType, staticTyping, vts, valueSerializer); } + @Deprecated // since 3.1.0 public IndexedListSerializer(IndexedListSerializer src, TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, BeanProperty property) { - super(src, vts, valueSerializer, unwrapSingle, property); + this(src, vts, valueSerializer, unwrapSingle, property, src._suppressableValue, src._suppressNulls); + } + + /** + * @since 3.1.0 + */ + public IndexedListSerializer(IndexedListSerializer src, + TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, + BeanProperty property, Object suppressableValue, boolean suppressNulls) { + super(src, vts, valueSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } @Override @@ -45,6 +55,15 @@ public IndexedListSerializer withResolved(BeanProperty property, return new IndexedListSerializer(this, vts, elementSerializer, unwrapSingle, property); } + @Override + public IndexedListSerializer withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IndexedListSerializer(this, vts, elementSerializer, unwrapSingle, property, + suppressableValue, suppressNulls); + } + + /* /********************************************************************** /* Accessors @@ -71,26 +90,55 @@ public final void serialize(Object value0, JsonGenerator gen, SerializationConte 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(Object value0, JsonGenerator g, SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value0, g, ctxt, false); + } + + @Override + public void serializeFilteredContents(Object value, JsonGenerator g, SerializationContext provider) + throws JacksonException + { + serializeContentsImpl(value, g, provider, true); + } + + private void serializeContentsImpl(Object value0, JsonGenerator g, SerializationContext ctxt, boolean filtered) + throws JacksonException { final List value = (List) value0; if (_elementSerializer != null) { - serializeContentsUsing(value, g, ctxt, _elementSerializer); + if (filtered) { + serializeFilteredContentsUsing(value, g, ctxt, _elementSerializer); + } else { + serializeContentsUsing(value, g, ctxt, _elementSerializer); + } return; } if (_valueTypeSerializer != null) { - serializeTypedContents(value, g, ctxt); + if (filtered) { + serializeFilteredTypedContents(value, g, ctxt); + } else { + serializeTypedContents(value, g, ctxt); + } return; } final int len = value.size(); @@ -102,6 +150,9 @@ public void serializeContents(Object value0, JsonGenerator g, SerializationConte for (; i < len; ++i) { Object elem = value.get(i); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } ctxt.defaultSerializeNullValue(g); } else { Class cc = elem.getClass(); @@ -115,6 +166,10 @@ public void serializeContents(Object value0, JsonGenerator g, SerializationConte serializer = _findAndAddDynamic(ctxt, cc); } } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + continue; + } serializer.serialize(elem, g, ctxt); } } @@ -123,9 +178,23 @@ public void serializeContents(Object value0, JsonGenerator g, SerializationConte } } - public void serializeContentsUsing(List value, JsonGenerator jgen, SerializationContext provider, + public void serializeContentsUsing(List value, JsonGenerator jgen, SerializationContext ctxt, ValueSerializer ser) throws JacksonException + { + serializeContentsUsingImpl(value, jgen, ctxt, ser, false); + } + + private void serializeFilteredContentsUsing(List value, JsonGenerator jgen, SerializationContext ctxt, + ValueSerializer ser) + throws JacksonException + { + serializeContentsUsingImpl(value, jgen, ctxt, ser, true); + } + + private void serializeContentsUsingImpl(List value, JsonGenerator jgen, SerializationContext ctxt, + ValueSerializer ser, boolean filtered) + throws JacksonException { final int len = value.size(); if (len == 0) { @@ -136,20 +205,41 @@ public void serializeContentsUsing(List value, JsonGenerator jgen, Serializat Object elem = value.get(i); try { if (elem == null) { - provider.defaultSerializeNullValue(jgen); - } else if (typeSer == null) { - ser.serialize(elem, jgen, provider); + if (filtered && _suppressNulls) { + continue; + } + ctxt.defaultSerializeNullValue(jgen); } else { - ser.serializeWithType(elem, jgen, provider, typeSer); + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, ser, ctxt)) { + continue; + } + if (typeSer == null) { + ser.serialize(elem, jgen, ctxt); + } else { + ser.serializeWithType(elem, jgen, ctxt, typeSer); + } } } catch (Exception e) { // [JACKSON-55] Need to add reference information - wrapAndThrow(provider, e, value, i); + wrapAndThrow(ctxt, e, value, i); } } } - public void serializeTypedContents(List value, JsonGenerator g, SerializationContext ctxt) + public void serializeTypedContents(List value, JsonGenerator jgen, SerializationContext ctxt) + throws JacksonException + { + serializeTypedContentsImpl(value, jgen, ctxt, false); + } + + public void serializeFilteredTypedContents(List value, JsonGenerator jgen, SerializationContext ctxt) + throws JacksonException + { + serializeTypedContentsImpl(value, jgen, ctxt, true); + } + + private void serializeTypedContentsImpl(List value, JsonGenerator jgen, SerializationContext ctxt, boolean filtered) throws JacksonException { final int len = value.size(); @@ -163,7 +253,10 @@ public void serializeTypedContents(List value, JsonGenerator g, Serialization for (; i < len; ++i) { Object elem = value.get(i); if (elem == null) { - ctxt.defaultSerializeNullValue(g); + if (filtered && _suppressNulls) { + continue; + } + ctxt.defaultSerializeNullValue(jgen); } else { Class cc = elem.getClass(); ValueSerializer serializer = serializers.serializerFor(cc); @@ -176,7 +269,11 @@ public void serializeTypedContents(List value, JsonGenerator g, Serialization } serializers = _dynamicValueSerializers; } - serializer.serializeWithType(elem, g, ctxt, typeSer); + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + continue; + } + serializer.serializeWithType(elem, jgen, ctxt, typeSer); } } } catch (Exception e) { diff --git a/src/main/java/tools/jackson/databind/ser/jdk/IndexedStringListSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/IndexedStringListSerializer.java index 41f7ce62ae..67c12c28b4 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IndexedStringListSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IndexedStringListSerializer.java @@ -33,14 +33,29 @@ protected IndexedStringListSerializer() { super(List.class); } + @Deprecated // since 3.1.0 public IndexedStringListSerializer(IndexedStringListSerializer src, Boolean unwrapSingle) { - super(src, unwrapSingle); + this(src, unwrapSingle, src._suppressableValue, src._suppressNulls); + } + + /** + * @since 3.1.0 + */ + public IndexedStringListSerializer(IndexedStringListSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, unwrapSingle, suppressableValue, suppressNulls); } @Override public ValueSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle) { - return new IndexedStringListSerializer(this, unwrapSingle); + return new IndexedStringListSerializer(this, unwrapSingle, null, false); + } + + @Override + public ValueSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle, + Object suppressableValue, boolean suppressNulls) { + return new IndexedStringListSerializer(this, unwrapSingle, suppressableValue, suppressNulls); } @Override protected JsonNode contentSchema() { return createSchemaNode("string", true); } @@ -65,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(); } @@ -88,14 +111,33 @@ public void serializeWithType(List value, JsonGenerator g, Serialization private final void serializeContents(List value, JsonGenerator g, SerializationContext provider, int len) throws JacksonException + { + serializeContentsImpl(value, g, provider, len, false); + } + + private final void serializeFilteredContents(List value, JsonGenerator g, + SerializationContext provider, int len) throws JacksonException + { + serializeContentsImpl(value, g, provider, len, true); + } + + private final void serializeContentsImpl(List value, JsonGenerator g, + SerializationContext provider, int len, boolean filtered) throws JacksonException { int i = 0; try { for (; i < len; ++i) { String str = value.get(i); if (str == null) { + if (filtered && _suppressNulls) { + continue; + } provider.defaultSerializeNullValue(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/tools/jackson/databind/ser/jdk/IterableSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java index cafde9ee98..2696f7e77f 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java @@ -18,10 +18,20 @@ public IterableSerializer(JavaType elemType, boolean staticTyping, super(Iterable.class, elemType, staticTyping, vts, null); } + @Deprecated // since 3.1.0 public IterableSerializer(IterableSerializer src, TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, BeanProperty property) { - super(src, vts, valueSerializer, unwrapSingle, property); + this(src, vts, valueSerializer, unwrapSingle, property, null, false); + } + + /** + * @since 3.1.0 + */ + public IterableSerializer(IterableSerializer src, + TypeSerializer vts, ValueSerializer valueSerializer, + Boolean unwrapSingle, BeanProperty property, Object suppressableValue, boolean suppressNulls) { + super(src, vts, valueSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } @Override @@ -36,6 +46,13 @@ public IterableSerializer withResolved(BeanProperty property, return new IterableSerializer(this, vts, elementSerializer, unwrapSingle, property); } + @Override + public IterableSerializer withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IterableSerializer(this, vts, elementSerializer, unwrapSingle, property, suppressableValue, suppressNulls); + } + /* /********************************************************************** /* Accessors @@ -113,4 +130,12 @@ public void serializeContents(Iterable value, JsonGenerator g, } while (it.hasNext()); } } + + @Override + protected void serializeFilteredContents(Iterable value, JsonGenerator g, SerializationContext ctxt) + throws JacksonException + { + // TODO: Implement later? + serializeContents(value, g, ctxt); + } } diff --git a/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java index 1062ceb765..65a8f669bc 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java @@ -18,10 +18,21 @@ public IteratorSerializer(JavaType elemType, boolean staticTyping, TypeSerialize super(Iterator.class, elemType, staticTyping, vts, null); } + @Deprecated // since 3.1.0 public IteratorSerializer(IteratorSerializer src, TypeSerializer vts, ValueSerializer valueSerializer, Boolean unwrapSingle, BeanProperty property) { - super(src, vts, valueSerializer, unwrapSingle, property); + this(src, vts, valueSerializer, unwrapSingle, property, null, false); + } + + /** + * @since 3.1.0 + */ + public IteratorSerializer(IteratorSerializer src, + TypeSerializer vts, ValueSerializer valueSerializer, + Boolean unwrapSingle, BeanProperty property, + Object suppressableValue, boolean suppressNulls) { + super(src, vts, valueSerializer, unwrapSingle, property, suppressableValue, suppressNulls); } @Override @@ -33,7 +44,16 @@ protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) public IteratorSerializer withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, Boolean unwrapSingle) { - return new IteratorSerializer(this, vts, elementSerializer, unwrapSingle, property); + return new IteratorSerializer(this, vts, elementSerializer, unwrapSingle, property, + null, false); + } + + @Override + public IteratorSerializer withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new IteratorSerializer(this, vts, elementSerializer, unwrapSingle, property, + suppressableValue, suppressNulls); } /* @@ -70,32 +90,62 @@ public final void serialize(Iterator value, JsonGenerator gen, } */ 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, - SerializationContext provider) + SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, false); + } + + @Override + protected void serializeFilteredContents(Iterator value, JsonGenerator g, + SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, true); + } + + private void serializeContentsImpl(Iterator value, JsonGenerator g, + SerializationContext ctxt, boolean filtered) throws JacksonException { if (!value.hasNext()) { return; } ValueSerializer serializer = _elementSerializer; if (serializer == null) { - _serializeDynamicContents(value, g, provider); + if (filtered) { + _serializeFilteredDynamicContents(value, g, ctxt); + } else { + _serializeDynamicContents(value, g, ctxt); + } return; } final TypeSerializer typeSer = _valueTypeSerializer; do { Object elem = value.next(); if (elem == null) { - provider.defaultSerializeNullValue(g); - } else if (typeSer == null) { - serializer.serialize(elem, g, provider); + if (filtered && _suppressNulls) { + continue; + } + ctxt.defaultSerializeNullValue(g); } else { - serializer.serializeWithType(elem, g, provider, typeSer); + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + continue; + } + if (typeSer == null) { + serializer.serialize(elem, g, ctxt); + } else { + serializer.serializeWithType(elem, g, ctxt, typeSer); + } } } while (value.hasNext()); } @@ -103,11 +153,26 @@ public void serializeContents(Iterator value, JsonGenerator g, protected void _serializeDynamicContents(Iterator value, JsonGenerator g, SerializationContext ctxt) throws JacksonException + { + _serializeDynamicContentsImpl(value, g, ctxt, false); + } + + protected void _serializeFilteredDynamicContents(Iterator value, JsonGenerator g, + SerializationContext ctxt) throws JacksonException + { + _serializeDynamicContentsImpl(value, g, ctxt, true); + } + + private void _serializeDynamicContentsImpl(Iterator value, JsonGenerator g, + SerializationContext ctxt, boolean filtered) throws JacksonException { final TypeSerializer typeSer = _valueTypeSerializer; do { Object elem = value.next(); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } ctxt.defaultSerializeNullValue(g); continue; } @@ -121,6 +186,10 @@ protected void _serializeDynamicContents(Iterator value, JsonGenerator g, serializer = _findAndAddDynamic(ctxt, cc); } } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + continue; + } if (typeSer == null) { serializer.serialize(elem, g, ctxt); } else { 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 e91b9fa0a9..8c5fb18fa5 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/StaticListSerializerBase.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; import tools.jackson.core.JacksonException; import tools.jackson.core.JsonGenerator; import tools.jackson.databind.*; @@ -13,6 +14,8 @@ import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import tools.jackson.databind.jsontype.TypeSerializer; import tools.jackson.databind.ser.std.StdSerializer; +import tools.jackson.databind.util.ArrayBuilders; +import tools.jackson.databind.util.BeanUtil; /** * Intermediate base class for Lists, Collections and Arrays @@ -21,6 +24,9 @@ public abstract class StaticListSerializerBase> extends StdSerializer { + // since 3.1.0 + 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 @@ -28,20 +34,59 @@ public abstract class StaticListSerializerBase> */ protected final Boolean _unwrapSingle; + /** + * Value that indicates suppression mechanism to use for + * content values (elements of container), if any; null + * for no filtering. + * @since 3.1.0 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * @since 3.1.0 + */ + protected final boolean _suppressNulls; + protected StaticListSerializerBase(Class cls) { super(cls); _unwrapSingle = null; + _suppressableValue = null; + _suppressNulls = false; } + @Deprecated // since 3.1.0 protected StaticListSerializerBase(StaticListSerializerBase src, Boolean unwrapSingle) { + this(src, unwrapSingle, src._suppressableValue, src._suppressNulls); + } + + /** + * @since 3.1.0 + */ + protected StaticListSerializerBase(StaticListSerializerBase src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { super(src); _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } + @Deprecated // since 3.1.0 public abstract ValueSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle); + /** + * To support `@JsonInclude`. + * Default implementation fallback to {@link StaticListSerializerBase#_withResolved(BeanProperty, Boolean, Object, boolean)} + * @since 3.1.0 + */ + public ValueSerializer _withResolved(BeanProperty prop, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls + ) { + return _withResolved(prop, unwrapSingle); + } + /* /********************************************************** /* Post-processing @@ -73,12 +118,62 @@ public ValueSerializer createContextual(SerializationContext 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 @@ -113,4 +208,34 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t @Override public abstract void serializeWithType(T value, JsonGenerator g, SerializationContext provider, TypeSerializer typeSer) throws JacksonException; + + /** + * 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 ctxt {@link SerializationContext} + * @return true if element should be serialized, false if suppressed + * + * @since 2.21 + */ + protected final boolean _shouldSerializeElement(Object elem, ValueSerializer serializer, + SerializationContext ctxt) throws JacksonException + { + if (_suppressableValue == null) { + return true; + } + if (_suppressableValue == MARKER_FOR_EMPTY) { + if (serializer != null) { + return !serializer.isEmpty(ctxt, 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/tools/jackson/databind/ser/jdk/StringCollectionSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/StringCollectionSerializer.java index c3747272d0..eff4e9bdc1 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/StringCollectionSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/StringCollectionSerializer.java @@ -33,15 +33,30 @@ protected StringCollectionSerializer() { super(Collection.class); } + @Deprecated // since 3.1.0 protected StringCollectionSerializer(StringCollectionSerializer src, Boolean unwrapSingle) { - super(src, unwrapSingle); + this(src, unwrapSingle, null, false); + } + + /** + * @since 3.1.0p + */ + protected StringCollectionSerializer(StringCollectionSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) + { + super(src, unwrapSingle, suppressableValue, suppressNulls); } @Override public ValueSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle) { - return new StringCollectionSerializer(this, unwrapSingle); + return new StringCollectionSerializer(this, unwrapSingle, null, false); + } + + @Override + public ValueSerializer _withResolved(BeanProperty prop, Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + return new StringCollectionSerializer(this, unwrapSingle, suppressableValue, suppressNulls); } @Override @@ -69,12 +84,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(); } @@ -91,22 +114,45 @@ public void serializeWithType(Collection value, JsonGenerator g, } private final void serializeContents(Collection value, JsonGenerator g, - SerializationContext provider) + SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, false); + } + + private final void serializeFilteredContents(Collection value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + serializeContentsImpl(value, g, ctxt, true); + } + + private final void serializeContentsImpl(Collection value, JsonGenerator g, + SerializationContext ctxt, boolean filtered) + throws JacksonException { int i = 0; try { for (String str : value) { if (str == null) { - provider.defaultSerializeNullValue(g); + if (filtered && _suppressNulls) { + ++i; + continue; + } + ctxt.defaultSerializeNullValue(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(str, null, ctxt)) { + ++i; + continue; + } g.writeString(str); } ++i; } } catch (Exception e) { - wrapAndThrow(provider, e, value, i); + wrapAndThrow(ctxt, e, value, i); } } } 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 ab2c6ec985..47bac61f1c 100644 --- a/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java +++ b/src/main/java/tools/jackson/databind/ser/std/AsArraySerializerBase.java @@ -4,12 +4,15 @@ import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; import tools.jackson.core.*; import tools.jackson.core.type.WritableTypeId; import tools.jackson.databind.*; import tools.jackson.databind.introspect.AnnotatedMember; import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.util.ArrayBuilders; +import tools.jackson.databind.util.BeanUtil; /** * Base class for serializers that will output contents as JSON @@ -23,6 +26,22 @@ public abstract class AsArraySerializerBase protected final boolean _staticTyping; + public final static Object MARKER_FOR_EMPTY = JsonInclude.Include.NON_EMPTY; + + /** + * Value that indicates suppression mechanism to use for + * content values (elements of container), if any; null + * for no filtering. + * @since 3.1.0 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * @since 3.1.0 + */ + protected final boolean _suppressNulls; + /** * Setting for specific local override for "unwrap single element arrays": * true for enable unwrapping, false for preventing it, `null` for using @@ -53,7 +72,7 @@ public abstract class AsArraySerializerBase protected AsArraySerializerBase(Class cls, JavaType elementType, boolean staticTyping, TypeSerializer vts, ValueSerializer elementSerializer) { - this(cls, elementType, staticTyping, vts, elementSerializer, null); + this(cls, elementType, staticTyping, vts, elementSerializer, null, null); } /** @@ -71,15 +90,29 @@ protected AsArraySerializerBase(Class cls, JavaType elementType, boolean stat _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = null; + _suppressNulls = false; + } + + @Deprecated // since 3.1.0 + @SuppressWarnings("unchecked") + protected AsArraySerializerBase(Class cls, JavaType elementType, boolean staticTyping, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, BeanProperty property) + { + this(cls, elementType, staticTyping, vts, elementSerializer, unwrapSingle, property, null, false); } /** * General purpose constructor. Use contextual constructors, if possible. + * + * @since 3.1.0 */ @SuppressWarnings("unchecked") protected AsArraySerializerBase(Class cls, JavaType elementType, boolean staticTyping, TypeSerializer vts, ValueSerializer elementSerializer, - Boolean unwrapSingle, BeanProperty property) + Boolean unwrapSingle, BeanProperty property, + Object suppressableValue, boolean suppressNulls) { super(cls, property); _elementType = elementType; @@ -88,8 +121,11 @@ protected AsArraySerializerBase(Class cls, JavaType elementType, boolean stat _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } + @Deprecated // since 3.1.0 @SuppressWarnings("unchecked") protected AsArraySerializerBase(AsArraySerializerBase src, TypeSerializer vts, ValueSerializer elementSerializer, @@ -101,12 +137,39 @@ protected AsArraySerializerBase(AsArraySerializerBase src, _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = src._suppressableValue; + _suppressNulls = src._suppressNulls; + } + + /** + * @since 3.1.0 + */ + @SuppressWarnings("unchecked") + protected AsArraySerializerBase(AsArraySerializerBase src, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, BeanProperty property, + Object suppressableValue, boolean suppressNulls) + { + super(src, property); + _elementType = src._elementType; + _staticTyping = src._staticTyping; + _valueTypeSerializer = vts; + _elementSerializer = (ValueSerializer) elementSerializer; + _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } + @Deprecated // since 3.1.0 protected abstract AsArraySerializerBase withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, Boolean unwrapSingle); + + protected abstract AsArraySerializerBase withResolved(BeanProperty property, + TypeSerializer vts, ValueSerializer elementSerializer, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls); + /* /********************************************************************** /* Post-processing @@ -157,11 +220,62 @@ public ValueSerializer createContextual(SerializationContext ctxt, } } } + + + // Handle content inclusion (similar to MapSerializer lines 560-609) + JsonInclude.Value inclV = findIncludeOverrides(ctxt, 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 = ctxt.includeFilterInstance(null, inclV.getContentFilter()); + if (valueToSuppress == null) { + suppressNulls = true; + } else { + suppressNulls = ctxt.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; } @@ -224,6 +338,18 @@ public void serializeWithType(T value, JsonGenerator g, SerializationContext ctx protected abstract void serializeContents(T value, JsonGenerator gen, SerializationContext provider) throws JacksonException; + /** + * Support `@JsonInclude` + * Will fallback to {@link AsArraySerializerBase#serializeContents(Object, JsonGenerator, SerializationContext)} for backward compatibility. + * + * @since 3.1.0 + */ + protected void serializeFilteredContents(T value, JsonGenerator g, SerializationContext provider) + throws JacksonException + { + serializeContents(value, g, provider); + } + @Override public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JacksonException @@ -238,4 +364,34 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } visitArrayFormat(visitor, typeHint, valueSer, _elementType); } + + /** + * 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 3.1.0 + */ + protected final boolean _shouldSerializeElement(Object elem, ValueSerializer serializer, + SerializationContext provider) throws JacksonException + { + 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/tools/jackson/databind/ser/filter/JsonIncludeForCollection5369Test.java b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeForCollection5369Test.java new file mode 100644 index 0000000000..4c87829cff --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeForCollection5369Test.java @@ -0,0 +1,229 @@ +package tools.jackson.databind.ser.filter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [databind#5369] Support `@JsonInclude` for collection +public class JsonIncludeForCollection5369Test + 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 = jsonMapperBuilder().build(); + + @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)); + } + + +} \ No newline at end of file