JsonIncludeContainerEmpty6065Test.java
package tools.jackson.databind.ser.filter;
import java.util.*;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonInclude;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertEquals;
// [databind#6065]: SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS should
// drop containers that become empty once content @JsonInclude filtering is applied,
// matching the behavior already implemented for Maps (#1649).
public class JsonIncludeContainerEmpty6065Test extends DatabindTestUtil
{
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class Bean {
public String myString;
public List<String> myList;
public Map<String, String> myMap;
}
// List of non-String elements (routed through IndexedListSerializer)
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class IntListBean {
public List<Integer> values;
}
// Set (routed through CollectionSerializer)
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class StringSetBean {
public Set<String> values;
}
// String array (routed through StringArraySerializer)
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class StringArrayBean {
public String[] values;
}
// Object array (routed through ObjectArraySerializer)
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class IntArrayBean {
public Integer[] values;
}
// Iterable (routed through IterableSerializer): must be a genuine non-Collection
// Iterable, otherwise runtime-type resolution routes it through CollectionSerializer
static class StringIterable implements Iterable<String> {
private final List<String> _values;
StringIterable(String... values) { _values = Arrays.asList(values); }
@Override public Iterator<String> iterator() { return _values.iterator(); }
}
// Primitive arrays (routed through JDKArraySerializers.*ArraySerializer): content
// NON_DEFAULT suppresses default-valued elements (0 / false), which is the only
// content inclusion that can suppress primitives (they are never "empty"/null).
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_DEFAULT)
static class PrimArraysBean {
public int[] ints;
public long[] longs;
public short[] shorts;
public double[] doubles;
public float[] floats;
public boolean[] bools;
}
// EnumSet (routed through EnumSetSerializer): enums are never null/empty, so only a
// CUSTOM content filter can suppress elements.
enum Size { SMALL, LARGE }
// Custom @JsonInclude content filter: its equals() returns true for values to suppress
static class SuppressSmallFilter {
@Override public boolean equals(Object other) { return other == Size.SMALL; }
@Override public int hashCode() { return 0; }
}
@JsonInclude(value = JsonInclude.Include.NON_EMPTY,
content = JsonInclude.Include.CUSTOM, contentFilter = SuppressSmallFilter.class)
static class EnumSetBean {
public EnumSet<Size> values;
}
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class IterableBean {
public Iterable<String> values;
}
// Nested containers: content filtering recurses, so an inner container that becomes
// empty is itself suppressed as an element of the outer container
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class NestedListBean {
public List<List<String>> values;
}
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_EMPTY)
static class MapOfListsBean {
public Map<String, List<String>> values;
}
// content = NON_NULL: only nulls are suppressed, empty Strings are retained
@JsonInclude(value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_NULL)
static class NonNullContentBean {
public List<String> strings;
public List<Integer> numbers;
public String[] stringArray;
public Integer[] numberArray;
}
private final ObjectMapper MAPPER = JsonMapper.builder()
.enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
.build();
private final ObjectMapper NO_FEATURE = JsonMapper.builder()
.disable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
.build();
@Test
public void allNull() throws Exception {
assertEquals("{}", MAPPER.writeValueAsString(new Bean()));
}
@Test
public void emptyContainers() throws Exception {
Bean bean = new Bean();
bean.myList = new ArrayList<>();
bean.myMap = new HashMap<>();
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void containersEmptyAfterContentFilter() throws Exception {
Bean bean = new Bean();
bean.myList = new ArrayList<>(Collections.singletonList(null));
bean.myMap = new HashMap<>();
bean.myMap.put("1", null);
// Both list and map become empty once null content is suppressed
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void stringListWithEmptyStringOnly() throws Exception {
Bean bean = new Bean();
bean.myList = new ArrayList<>(Arrays.asList("", ""));
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void stringListKeepsNonEmpty() throws Exception {
Bean bean = new Bean();
bean.myList = new ArrayList<>(Arrays.asList(null, "keep", ""));
assertEquals("""
{"myList":["keep"]}""",
MAPPER.writeValueAsString(bean));
}
@Test
public void intListWithNullsOnly() throws Exception {
IntListBean bean = new IntListBean();
bean.values = new ArrayList<>(Arrays.asList(null, null));
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void intListKeepsValues() throws Exception {
IntListBean bean = new IntListBean();
bean.values = new ArrayList<>(Arrays.asList(null, 42));
assertEquals("""
{"values":[42]}""",
MAPPER.writeValueAsString(bean));
}
@Test
public void stringSetWithNullsOnly() throws Exception {
StringSetBean bean = new StringSetBean();
bean.values = new LinkedHashSet<>(Arrays.asList((String) null));
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
// [databind#6065]: same handling for String arrays
@Test
public void stringArrayEmptyAfterContentFilter() throws Exception {
StringArrayBean bean = new StringArrayBean();
bean.values = new String[] { null, "" };
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void stringArrayKeepsNonEmpty() throws Exception {
StringArrayBean bean = new StringArrayBean();
bean.values = new String[] { null, "keep", "" };
assertEquals("""
{"values":["keep"]}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: same handling for Object arrays
@Test
public void objectArrayNullsOnly() throws Exception {
IntArrayBean bean = new IntArrayBean();
bean.values = new Integer[] { null, null };
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void objectArrayKeepsValues() throws Exception {
IntArrayBean bean = new IntArrayBean();
bean.values = new Integer[] { null, 42 };
assertEquals("""
{"values":[42]}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: same handling for Iterable
@Test
public void iterableEmptyAfterContentFilter() throws Exception {
IterableBean bean = new IterableBean();
bean.values = new StringIterable(null, "");
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void iterableKeepsNonEmpty() throws Exception {
IterableBean bean = new IterableBean();
bean.values = new StringIterable(null, "keep", "");
assertEquals("""
{"values":["keep"]}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: primitive arrays, all-default content suppressed -> property dropped
@Test
public void primitiveArraysAllDefault() throws Exception {
PrimArraysBean bean = new PrimArraysBean();
bean.ints = new int[] { 0, 0 };
bean.longs = new long[] { 0L };
bean.shorts = new short[] { 0, 0 };
bean.doubles = new double[] { 0.0, 0.0 };
bean.floats = new float[] { 0.0f };
bean.bools = new boolean[] { false, false };
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void primitiveArraysKeepNonDefault() throws Exception {
PrimArraysBean bean = new PrimArraysBean();
bean.ints = new int[] { 0, 42 };
bean.longs = new long[] { 0L, 7L };
bean.doubles = new double[] { 0.0, 1.5 };
bean.bools = new boolean[] { false, true };
assertEquals("""
{"bools":[true],"doubles":[1.5],"ints":[42],"longs":[7]}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: EnumSet, all elements suppressed by CUSTOM filter -> property dropped
@Test
public void enumSetAllSuppressed() throws Exception {
EnumSetBean bean = new EnumSetBean();
bean.values = EnumSet.of(Size.SMALL);
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void enumSetKeepsNonSuppressed() throws Exception {
EnumSetBean bean = new EnumSetBean();
bean.values = EnumSet.of(Size.SMALL, Size.LARGE);
assertEquals("""
{"values":["LARGE"]}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: nested containers -- inner List that is empty after content
// filtering is itself suppressed as an element of the outer List
@Test
public void nestedListsAllEmptyAfterContentFilter() throws Exception {
NestedListBean bean = new NestedListBean();
bean.values = new ArrayList<>(Arrays.asList(
new ArrayList<>(Arrays.asList("", "")),
new ArrayList<>(Collections.singletonList((String) null)),
new ArrayList<>()));
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void nestedListsKeepNonEmpty() throws Exception {
NestedListBean bean = new NestedListBean();
bean.values = new ArrayList<>(Arrays.asList(
new ArrayList<>(Arrays.asList("", "")),
new ArrayList<>(Arrays.asList(null, "keep"))));
assertEquals("""
{"values":[["keep"]]}""",
MAPPER.writeValueAsString(bean));
}
@Test
public void mapOfListsAllEmptyAfterContentFilter() throws Exception {
MapOfListsBean bean = new MapOfListsBean();
bean.values = new LinkedHashMap<>();
bean.values.put("a", new ArrayList<>(Arrays.asList("", "")));
bean.values.put("b", new ArrayList<>());
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void mapOfListsKeepsNonEmpty() throws Exception {
MapOfListsBean bean = new MapOfListsBean();
bean.values = new LinkedHashMap<>();
bean.values.put("a", new ArrayList<>(Collections.singletonList("")));
bean.values.put("b", new ArrayList<>(Arrays.asList(null, "keep")));
assertEquals("""
{"values":{"b":["keep"]}}""",
MAPPER.writeValueAsString(bean));
}
// [databind#6065]: content = NON_NULL suppresses nulls only; container with
// nothing but nulls becomes empty, but empty Strings/zeroes are retained
@Test
public void nonNullContentAllNulls() throws Exception {
NonNullContentBean bean = new NonNullContentBean();
bean.strings = new ArrayList<>(Arrays.asList(null, null));
bean.numbers = new ArrayList<>(Collections.singletonList((Integer) null));
bean.stringArray = new String[] { null };
bean.numberArray = new Integer[] { null, null };
assertEquals("{}", MAPPER.writeValueAsString(bean));
}
@Test
public void nonNullContentRetainsEmptyValues() throws Exception {
NonNullContentBean bean = new NonNullContentBean();
bean.strings = new ArrayList<>(Arrays.asList(null, ""));
bean.numbers = new ArrayList<>(Arrays.asList(null, 0));
bean.stringArray = new String[] { null, "" };
bean.numberArray = new Integer[] { null, 0 };
assertEquals("""
{"numberArray":[0],"numbers":[0],"stringArray":[""],"strings":[""]}""",
MAPPER.writeValueAsString(bean));
}
// Without the feature, containers are left intact (only null containers dropped)
@Test
public void featureDisabledLeavesContainers() throws Exception {
Bean bean = new Bean();
bean.myList = new ArrayList<>(Collections.singletonList(null));
assertEquals("""
{"myList":[null]}""",
NO_FEATURE.writeValueAsString(bean));
}
}