Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,11 @@ Théo Szanto (@indyteo)
* Fixed #6043: `FAIL_ON_UNKNOWN_PROPERTIES` has no effect with `JsonFormat.Shape.ARRAY`
when using creator-based instantiation (such as `record`)
[3.2.1]
* Fixed #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully
remove empty collection during serialization
[3.2.1]

@doeiqts
* Reported #6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully
remove empty collection during serialization
[3.2.1]
4 changes: 4 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ No changes since 3.2
(reported by @sebastien-leveque)
(fix by @cowtowncoder, w/ Claude code)
#6060: `@JsonView` by-passed for `@JsonUnwrapped` Field/Setter properties [CVE-2026-59889]
#6065: `SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS` does not fully
remove empty collection during serialization
(reported by @doeiqts)
(fix by @seonwooj0810)
#6077: `FAIL_ON_UNEXPECTED_VIEW_PROPERTIES` not being respected with constructor-backed properties
(nor with Builders, `@JsonUnwrapped`, external type ids or `Shape.ARRAY`)
(reported by João G)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ protected CollectionSerializer withResolved(BeanProperty property,

@Override
public boolean isEmpty(SerializationContext prov, Collection<?> value) {
return value.isEmpty();
if (value.isEmpty()) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// a collection whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
return _allElementsSuppressed(prov, value.iterator());
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ public EnumSetSerializer withResolved(BeanProperty property,

@Override
public boolean isEmpty(SerializationContext prov, EnumSet<? extends Enum<?>> value) {
return value.isEmpty();
if (value.isEmpty()) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an EnumSet whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
return _allElementsSuppressed(prov, value.iterator());
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@ public IndexedListSerializer withResolved(BeanProperty property,

@Override
public boolean isEmpty(SerializationContext prov, Object value) {
return ((List<?>)value).isEmpty();
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// a list whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
return _allElementsSuppressed(prov, list.iterator());
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,22 @@ public IterableSerializer withResolved(BeanProperty property,

@Override
public boolean isEmpty(SerializationContext ctxt, Iterable<?> value) {
// Not really good way to implement this, but has to do for now:
return !value.iterator().hasNext();
Iterator<?> it = value.iterator();
if (!it.hasNext()) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an Iterable whose every element is suppressed is considered empty.
// NOTE: unlike the old peek-only check, this consumes elements from the
// iterator; serialize()/serializeContents() obtain a fresh one via
// value.iterator(), so this relies on the Iterable being re-iterable --
// an assumption this serializer already makes (see hasSingleElement(),
// called from serialize() before serializeContents()). The cost is one
// extra traversal, only when content filtering is active.
if (_needToCheckFiltering(ctxt)) {
return _allElementsSuppressed(ctxt, it);
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public IteratorSerializer withResolved(BeanProperty property,

@Override
public boolean isEmpty(SerializationContext ctxt, Iterator<?> value) {
// [databind#6065]: unlike other containers, cannot apply content @JsonInclude
// filtering here: inspecting elements would consume the (single-use) Iterator,
// leaving nothing for `serialize()`. So an Iterator that becomes empty after
// content filtering is still emitted as `[]` rather than dropped.
return !value.hasNext();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, boolean[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (boolean v : value) {
if (_shouldSerializeElement(prov, Boolean.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down Expand Up @@ -226,7 +239,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, short[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (short v : value) {
if (_shouldSerializeElement(prov, Short.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down Expand Up @@ -378,7 +404,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, int[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (int v : value) {
if (_shouldSerializeElement(prov, Integer.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down Expand Up @@ -460,7 +499,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, long[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (long v : value) {
if (_shouldSerializeElement(prov, Long.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down Expand Up @@ -547,7 +599,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, float[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (float v : value) {
if (_shouldSerializeElement(prov, Float.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down Expand Up @@ -649,7 +714,20 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, double[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (double v : value) {
if (_shouldSerializeElement(prov, Double.valueOf(v))) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,15 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, Object[] value) {
return value.length == 0;
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
return _allElementsSuppressed(prov, value);
}
return false;
}

@Override
Expand Down Expand Up @@ -480,6 +488,49 @@ protected boolean _shouldSerializeElement(SerializationContext ctxt,
return !_suppressableValue.equals(elem);
}

/**
* Helper method for content-aware emptiness checks (used by {@link #isEmpty}):
* when content {@code @JsonInclude} is applied to containers, an array is
* considered empty if every one of its elements would be suppressed during
* serialization (so the array would render as an empty array). Callers should
* only invoke this when {@link #_needToCheckFiltering} returns true.
*
* @since 3.2.1
*/
protected boolean _allElementsSuppressed(SerializationContext ctxt, Object[] value)
{
for (Object elem : value) {
if (elem == null) {
if (_suppressNulls) {
continue;
}
return false;
}
// Only the "empty" check needs an element serializer; other suppression
// mechanisms compare the element directly, so avoid resolving one
ValueSerializer<Object> serializer = null;
if (_suppressableValue == MARKER_FOR_EMPTY) {
serializer = _elementSerializer;
if (serializer == null) {
Class<?> cc = elem.getClass();
serializer = _dynamicValueSerializers.serializerFor(cc);
if (serializer == null) {
if (_elementType.hasGenericTypes()) {
serializer = _findAndAddDynamic(ctxt,
ctxt.constructSpecializedType(_elementType, cc));
} else {
serializer = _findAndAddDynamic(ctxt, cc);
}
}
}
}
if (_shouldSerializeElement(ctxt, elem, serializer)) {
return false;
}
}
return true;
}

@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,26 @@ public ValueSerializer<?> createContextual(SerializationContext ctxt,

@Override
public boolean isEmpty(SerializationContext provider, T value) {
return (value == null) || (value.isEmpty());
if ((value == null) || value.isEmpty()) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// a collection whose every element is suppressed is considered empty
if (_needToCheckFiltering(provider)) {
for (Object elem : value) {
if (elem == null) {
if (!_suppressNulls) {
return false;
}
// Elements are "natural" types (Strings), never have content serializer
// (see `createContextual()`), so pass `null`, same as `serializeContents()`
} else if (_shouldSerializeElement(elem, null, provider)) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,26 @@ public ValueSerializer<?> getContentSerializer() {

@Override
public boolean isEmpty(SerializationContext prov, String[] value) {
return (value.length == 0);
if (value.length == 0) {
return true;
}
// [databind#6065]: with content @JsonInclude applied to containers,
// an array whose every element is suppressed is considered empty
if (_needToCheckFiltering(prov)) {
for (String str : value) {
if (str == null) {
if (_suppressNulls) {
continue;
}
return false;
}
if (_shouldSerializeElement(prov, str, _elementSerializer)) {
return false;
}
}
return true;
}
return false;
}

@Override
Expand Down
Loading