diff --git a/src/main/java/tools/jackson/databind/SerializationFeature.java b/src/main/java/tools/jackson/databind/SerializationFeature.java index e778426c9b8..e5c30c0f4a6 100644 --- a/src/main/java/tools/jackson/databind/SerializationFeature.java +++ b/src/main/java/tools/jackson/databind/SerializationFeature.java @@ -256,6 +256,24 @@ public enum SerializationFeature implements ConfigFeature */ FAIL_ON_ORDER_MAP_BY_INCOMPARABLE_KEY(false), + /** + * Feature that determines whether {@code JsonInclude#content()} is also + * applied to values inside {@link java.util.Collection} properties. + *

+ * By default, {@code content()} inclusion rules are only applied to + * {@code Map} values and reference types, and are ignored for + * {@code Collection} element values. + *

+ * When this feature is enabled, {@code JsonInclude#content()} rules + * are evaluated for {@code Collection} elements during serialization. + *

+ * This feature is disabled by default for backwards + * compatibility. + * + * @since 3.1 + */ + APPLY_JSON_INCLUDE_FOR_COLLECTIONS(false), + /* /********************************************************************** /* Other 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 a3d02c88714..42c41bd34be 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/CollectionSerializer.java @@ -47,23 +47,44 @@ public CollectionSerializer(JavaType elemType, boolean staticTyping, TypeSeriali _maybeEnumSet = elemType.isEnumType() || elemType.isJavaLangObject(); } + @Deprecated // since 3.1 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 + */ + 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; } @Override protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { - return new CollectionSerializer(this, vts, _elementSerializer, _unwrapSingle, _property); + return new CollectionSerializer(this, vts, _elementSerializer, _unwrapSingle, _property, + _suppressableValue, _suppressNulls); } + @Deprecated // @since 3.1 @Override 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 + @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); } /* @@ -98,22 +119,51 @@ public final void serialize(Collection value, JsonGenerator g, if (((_unwrapSingle == null) && ctxt.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } g.writeEndArray(); } @Override - public void serializeContents(Collection value, JsonGenerator g, + public void serializeContents(Collection value, JsonGenerator g, SerializationContext ctxt) + { + serializeContentsImpl(value, g, ctxt, false); + } + + @Override + protected void serializeFilteredContents(Collection value, JsonGenerator g, SerializationContext ctxt) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + 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(); @@ -130,6 +180,10 @@ public void serializeContents(Collection value, JsonGenerator g, do { Object elem = it.next(); if (elem == null) { + if (filtered && _suppressNulls) { + ++i; + continue; + } ctxt.defaultSerializeNullValue(g); } else { Class cc = elem.getClass(); @@ -142,6 +196,11 @@ public void serializeContents(Collection value, JsonGenerator g, } 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 { @@ -158,6 +217,20 @@ public void serializeContents(Collection value, JsonGenerator g, 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, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + private void serializeContentsUsingImpl(Collection value, JsonGenerator g, SerializationContext ctxt, + ValueSerializer ser, boolean filtered) throws JacksonException { Iterator it = value.iterator(); if (it.hasNext()) { @@ -169,8 +242,17 @@ public void serializeContentsUsing(Collection value, JsonGenerator g, Object elem = it.next(); try { if (elem == null) { + 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, ctxt); } else { 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 1d7f3425180..9dc34c3a023 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 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 + */ + 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 @@ -26,11 +37,19 @@ protected EnumSetSerializer _withValueTypeSerializer(TypeSerializer vts) { return this; } + @Deprecated // @since 3.1 @Override 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 +99,5 @@ public void serializeContents(EnumSet> value, JsonGenerator g, enumSer.serialize(en, g, 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 0902165f987..213eda74470 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IndexedListSerializer.java @@ -26,18 +26,31 @@ public IndexedListSerializer(JavaType elemType, boolean staticTyping, TypeSerial super(List.class, elemType, staticTyping, vts, valueSerializer); } + @Deprecated // since 3.1 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 + */ + 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 protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { return new IndexedListSerializer(this, - vts, _elementSerializer, _unwrapSingle, _property); + vts, _elementSerializer, _unwrapSingle, _property, + _suppressableValue, _suppressNulls); + } + @Deprecated // @since 3.1 @Override public IndexedListSerializer withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, @@ -45,6 +58,14 @@ 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 @@ -72,26 +93,61 @@ public final void serialize(Object value0, JsonGenerator g, if (((_unwrapSingle == null) && ctxt.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } g.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, + provider.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + 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(); @@ -103,6 +159,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(); @@ -116,6 +175,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); } } @@ -127,6 +190,21 @@ public void serializeContents(Object value0, JsonGenerator g, SerializationConte public void serializeContentsUsing(List value, JsonGenerator g, SerializationContext ctxt, ValueSerializer ser) throws JacksonException + { + serializeContentsUsingImpl(value, g, ctxt, ser, false); + } + + private void serializeFilteredContentsUsing(List value, JsonGenerator g, + SerializationContext ctxt, ValueSerializer ser) + throws JacksonException + { + serializeContentsUsingImpl(value, g, ctxt, ser, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + private void serializeContentsUsingImpl(List value, JsonGenerator g, + SerializationContext ctxt, ValueSerializer ser, boolean filtered) + throws JacksonException { final int len = value.size(); if (len == 0) { @@ -137,11 +215,20 @@ public void serializeContentsUsing(List value, JsonGenerator g, Object elem = value.get(i); try { if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } ctxt.defaultSerializeNullValue(g); - } else if (typeSer == null) { - ser.serialize(elem, g, ctxt); } else { - ser.serializeWithType(elem, g, ctxt, 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, g, ctxt); + } else { + ser.serializeWithType(elem, g, ctxt, typeSer); + } } } catch (Exception e) { // [JACKSON-55] Need to add reference information @@ -150,7 +237,23 @@ public void serializeContentsUsing(List value, JsonGenerator g, } } - public void serializeTypedContents(List value, JsonGenerator g, SerializationContext ctxt) + public void serializeTypedContents(List value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + serializeTypedContentsImpl(value, g, ctxt, false); + } + + public void serializeFilteredTypedContents(List value, JsonGenerator g, + SerializationContext ctxt) + throws JacksonException + { + serializeTypedContentsImpl(value, g, ctxt, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + private void serializeTypedContentsImpl(List value, JsonGenerator g, + SerializationContext ctxt, boolean filtered) throws JacksonException { final int len = value.size(); @@ -164,6 +267,9 @@ public void serializeTypedContents(List value, JsonGenerator g, Serialization for (; i < len; ++i) { Object elem = value.get(i); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } ctxt.defaultSerializeNullValue(g); } else { Class cc = elem.getClass(); @@ -177,6 +283,10 @@ public void serializeTypedContents(List value, JsonGenerator g, Serialization } serializers = _dynamicValueSerializers; } + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(elem, serializer, ctxt)) { + continue; + } serializer.serializeWithType(elem, g, ctxt, typeSer); } } 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 47fc5f4ff0d..54a049d10c3 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,30 @@ protected IndexedStringListSerializer() { super(List.class); } + @Deprecated // since 3.1 public IndexedStringListSerializer(IndexedStringListSerializer src, Boolean unwrapSingle) { - super(src, unwrapSingle); + this(src, unwrapSingle, src._suppressableValue, src._suppressNulls); } + /** + * @since 3.1 + */ + public IndexedStringListSerializer(IndexedStringListSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { + super(src, unwrapSingle, suppressableValue, suppressNulls); + } + + @Deprecated // @since 3.1 @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); } @@ -66,12 +82,24 @@ public void serialize(List value, JsonGenerator g, if (((_unwrapSingle == null) && ctxt.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, ctxt, 1); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt, 1); + } else { + serializeContents(value, g, ctxt, 1); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, ctxt, len); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt, len); + } else { + serializeContents(value, g, ctxt, len); + } g.writeEndArray(); } @@ -90,14 +118,36 @@ public void serializeWithType(List value, JsonGenerator g, private final void serializeContents(List value, JsonGenerator g, SerializationContext ctxt, int len) throws JacksonException + { + serializeContentsImpl(value, g, ctxt, len, + false); + } + + private final void serializeFilteredContents(List value, JsonGenerator g, + SerializationContext provider, int len) throws JacksonException + { + serializeContentsImpl(value, g, provider, len, + provider.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + private final void serializeContentsImpl(List value, JsonGenerator g, + SerializationContext ctxt, 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; + } ctxt.defaultSerializeNullValue(g); } else { + // Check if this element should be suppressed (only in filtered mode) + if (filtered && !_shouldSerializeElement(str, null, ctxt)) { + 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 81361df1b71..fa2b91928ed 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IterableSerializer.java @@ -18,17 +18,29 @@ public IterableSerializer(JavaType elemType, boolean staticTyping, super(Iterable.class, elemType, staticTyping, vts, null); } + @Deprecated // since 3.1 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 + */ + 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 protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { - return new IterableSerializer(this, vts, _elementSerializer, _unwrapSingle, _property); + return new IterableSerializer(this, vts, _elementSerializer, _unwrapSingle, _property, + _suppressableValue, _suppressNulls); } + @Deprecated // @since 3.1 @Override public IterableSerializer withResolved(BeanProperty property, TypeSerializer vts, ValueSerializer elementSerializer, @@ -36,6 +48,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 @@ -115,4 +134,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 19b79e199c9..b6ed2ef0215 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/IteratorSerializer.java @@ -18,22 +18,44 @@ public IteratorSerializer(JavaType elemType, boolean staticTyping, TypeSerialize super(Iterator.class, elemType, staticTyping, vts, null); } + @Deprecated // since 3.1 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 + */ + 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 protected StdContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { - return new IteratorSerializer(this, vts, _elementSerializer, _unwrapSingle, _property); + return new IteratorSerializer(this, vts, _elementSerializer, _unwrapSingle, _property, + _suppressableValue, _suppressNulls); } + @Deprecated // @since 3.1 @Override 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,7 +92,13 @@ public final void serialize(Iterator value, JsonGenerator g, } */ g.writeStartArray(value); - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } g.writeEndArray(); } @@ -78,24 +106,51 @@ public final void serialize(Iterator value, JsonGenerator g, public void serializeContents(Iterator value, JsonGenerator g, 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, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + 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, ctxt); + if (filtered) { + _serializeFilteredDynamicContents(value, g, ctxt); + } else { + _serializeDynamicContents(value, g, ctxt); + } return; } final TypeSerializer typeSer = _valueTypeSerializer; do { Object elem = value.next(); if (elem == null) { + if (filtered && _suppressNulls) { + continue; + } ctxt.defaultSerializeNullValue(g); - } else if (typeSer == null) { - serializer.serialize(elem, g, ctxt); } else { - serializer.serializeWithType(elem, g, ctxt, 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 +158,28 @@ 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, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + 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 +193,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 528bdb2e025..cd8aebd63d4 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 + 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,63 @@ 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 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * + * @since 3.1 + */ + protected final boolean _suppressNulls; + protected StaticListSerializerBase(Class cls) { super(cls); _unwrapSingle = null; + _suppressableValue = null; + _suppressNulls = false; } + @Deprecated // since 3.1 protected StaticListSerializerBase(StaticListSerializerBase src, Boolean unwrapSingle) { + this(src, unwrapSingle, src._suppressableValue, src._suppressNulls); + } + + /** + * @since 3.1 + */ + protected StaticListSerializerBase(StaticListSerializerBase src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) { super(src); _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } + // Being used by the new _withResolved(BeanProperty, Boolean, Object, boolean) method as fallback + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated // since 3.1 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 + */ + public ValueSerializer _withResolved(BeanProperty prop, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls + ) { + return _withResolved(prop, unwrapSingle); + } + /* /********************************************************** /* Post-processing @@ -73,12 +122,62 @@ public ValueSerializer createContextual(SerializationContext ctxt, if (ser == null) { ser = ctxt.findContentValueSerializer(String.class, property); } + // Handle content inclusion (similar to MapSerializer lines 560-609) + JsonInclude.Value inclV = findIncludeOverrides(ctxt, 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(ctxt.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 = 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; + } + } + } + // 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 @@ -112,6 +211,35 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t // just to make sure it gets implemented: @Override public abstract void serializeWithType(T value, JsonGenerator g, - SerializationContext ctxt, TypeSerializer typeSer) - throws JacksonException; + SerializationContext ctxt, 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 c18bdf21bd0..238a3c89b17 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,31 @@ protected StringCollectionSerializer() { super(Collection.class); } + @Deprecated // since 3.1 protected StringCollectionSerializer(StringCollectionSerializer src, Boolean unwrapSingle) { - super(src, unwrapSingle); + this(src, unwrapSingle, null, false); } + /** + * @since 3.1 + */ + protected StringCollectionSerializer(StringCollectionSerializer src, + Boolean unwrapSingle, Object suppressableValue, boolean suppressNulls) + { + super(src, unwrapSingle, suppressableValue, suppressNulls); + } + + @Deprecated // @since 3.1 @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 +85,24 @@ public void serialize(Collection value, JsonGenerator g, if (((_unwrapSingle == null) && ctxt.isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) || (_unwrapSingle == Boolean.TRUE)) { - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } return; } } g.writeStartArray(value, len); - serializeContents(value, g, ctxt); + if (ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + && ((_suppressableValue != null) || _suppressNulls) + ) { + serializeFilteredContents(value, g, ctxt); + } else { + serializeContents(value, g, ctxt); + } g.writeEndArray(); } @@ -93,14 +121,39 @@ public void serializeWithType(Collection value, JsonGenerator g, private final void serializeContents(Collection value, JsonGenerator g, 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, + ctxt.isEnabled(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS)); + } + + 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) { + 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; 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 333af2c588f..7334b2e085b 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,24 @@ 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 + */ + protected final Object _suppressableValue; + + /** + * Flag that indicates whether nulls should be suppressed. + * + * @since 3.1 + */ + 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 +74,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 +92,28 @@ protected AsArraySerializerBase(Class cls, JavaType elementType, boolean stat _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = null; + _suppressNulls = false; + } + + @Deprecated // since 3.1 + 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 */ @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 +122,11 @@ protected AsArraySerializerBase(Class cls, JavaType elementType, boolean stat _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = suppressableValue; + _suppressNulls = suppressNulls; } + @Deprecated // since 3.1 @SuppressWarnings("unchecked") protected AsArraySerializerBase(AsArraySerializerBase src, TypeSerializer vts, ValueSerializer elementSerializer, @@ -101,12 +138,39 @@ protected AsArraySerializerBase(AsArraySerializerBase src, _valueTypeSerializer = vts; _elementSerializer = (ValueSerializer) elementSerializer; _unwrapSingle = unwrapSingle; + _suppressableValue = src._suppressableValue; + _suppressNulls = src._suppressNulls; } + /** + * @since 3.1 + */ + @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 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 +221,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; } @@ -225,6 +340,18 @@ protected abstract void serializeContents(T value, JsonGenerator g, SerializationContext ctxt) throws JacksonException; + /** + * Support `@JsonInclude` + * Will fallback to {@link AsArraySerializerBase#serializeContents(Object, JsonGenerator, SerializationContext)} for backward compatibility. + * + * @since 3.1 + */ + 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 @@ -239,4 +366,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 + */ + 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 00000000000..56635754ef4 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/filter/JsonIncludeForCollection5369Test.java @@ -0,0 +1,387 @@ +package tools.jackson.databind.ser.filter; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import com.fasterxml.jackson.annotation.JsonInclude; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [databind#5369] Support `@JsonInclude` for collection +public class JsonIncludeForCollection5369Test + extends DatabindTestUtil +{ + /* + /********************************************************** + /* POJOs + Filters (kept together) + /********************************************************** + */ + + // String "foo" filter + static class FooFilter { + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + return "foo".equals(other); + } + } + + static class FooListBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = FooFilter.class) + public List items = new ArrayList<>(); + + FooListBean add(String value) { + items.add(value); + return this; + } + } + + static class FooSetBean { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = FooFilter.class) + public Set items = new LinkedHashSet<>(); + + FooSetBean add(String value) { + items.add(value); + return this; + } + } + + // NON_NULL + static class NonNullListBean { + @JsonInclude(content = JsonInclude.Include.NON_NULL) + public List items = new ArrayList<>(); + + NonNullListBean add(String value) { + items.add(value); + return this; + } + } + + // NON_EMPTY + static class NonEmptyListBean { + @JsonInclude(content = JsonInclude.Include.NON_EMPTY) + public List items = new ArrayList<>(); + + NonEmptyListBean add(String value) { + items.add(value); + return this; + } + } + + // NON_DEFAULT + static class NonDefaultListBean { + @JsonInclude(content = JsonInclude.Include.NON_DEFAULT) + public List items = new ArrayList<>(); + + NonDefaultListBean add(String value) { + items.add(value); + return this; + } + } + + // Integer = 42 filter + static class IntegerFilter { + @Override + public boolean equals(Object other) { + return Integer.valueOf(42).equals(other); + } + } + + static class IntegerListPojo { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = IntegerFilter.class) + public List values = new ArrayList<>(); + + IntegerListPojo add(int v) { + values.add(v); + return this; + } + } + + // Short = 7 filter + static class ShortFilter { + @Override + public boolean equals(Object other) { + return Short.valueOf((short) 7).equals(other); + } + } + + static class ShortListPojo { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = ShortFilter.class) + public List values = new ArrayList<>(); + + ShortListPojo add(short v) { + values.add(v); + return this; + } + } + + // Byte = 9 filter + static class ByteFilter { + @Override + public boolean equals(Object other) { + return Byte.valueOf((byte) 9).equals(other); + } + } + + static class ByteListPojo { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = ByteFilter.class) + public List values = new ArrayList<>(); + + ByteListPojo add(byte v) { + values.add(v); + return this; + } + } + + // Long = 100 filter + static class LongFilter { + @Override + public boolean equals(Object other) { + return Long.valueOf(100L).equals(other); + } + } + + static class LongListPojo { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = LongFilter.class) + public List values = new ArrayList<>(); + + LongListPojo add(long v) { + values.add(v); + return this; + } + } + + // Double = 1.25 filter + static class DoubleFilter { + @Override + public boolean equals(Object other) { + return Double.valueOf(1.25).equals(other); + } + } + + static class DoubleListPojo { + @JsonInclude(content = JsonInclude.Include.CUSTOM, + contentFilter = DoubleFilter.class) + public List values = new ArrayList<>(); + + DoubleListPojo add(double v) { + values.add(v); + return this; + } + } + + // Counting filter + static class CountingFooFilter { + static final AtomicInteger counter = new AtomicInteger(); + + @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<>(); + + CountingFooListBean add(String value) { + items.add(value); + return this; + } + } + + 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; + } + } + + /* + /********************************************************** + /* Mapper + /********************************************************** + */ + + private final ObjectMapper MAPPER = jsonMapperBuilder() + .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_COLLECTIONS) + .build(); + + /* + /********************************************************** + /* Tests + /********************************************************** + */ + + @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).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 testCustomFilterWithIntegerList() throws Exception { + IntegerListPojo input = new IntegerListPojo() + .add(1).add(42).add(2); + + assertEquals(a2q("{'values':[1,2]}"), + MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithShortList() throws Exception { + ShortListPojo input = new ShortListPojo() + .add((short) 1).add((short) 7).add((short) 2); + + assertEquals(a2q("{'values':[1,2]}"), + MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithByteList() throws Exception { + ByteListPojo input = new ByteListPojo() + .add((byte) 1).add((byte) 9).add((byte) 2); + + assertEquals(a2q("{'values':[1,2]}"), + MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithDoubleList() throws Exception { + DoubleListPojo input = new DoubleListPojo() + .add(0.5).add(1.25).add(2.5); + + assertEquals(a2q("{'values':[0.5,2.5]}"), + MAPPER.writeValueAsString(input)); + } + + @Test + public void testCustomFilterWithLongList() throws Exception { + LongListPojo input = new LongListPojo() + .add(10L).add(100L).add(20L); + + assertEquals(a2q("{'values':[10,20]}"), + 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) + ); + } + +}