JsonIncludeCollectionTest.java

package tools.jackson.databind.ser.filter;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.testutil.DatabindTestUtil;
import tools.jackson.databind.testutil.NoCheckSubTypeValidator;

import static org.junit.jupiter.api.Assertions.*;

public class JsonIncludeCollectionTest extends DatabindTestUtil
{
    // Basic NON_EMPTY on EnumSet field
    static class NonEmptyEnumSet {
        @JsonInclude(JsonInclude.Include.NON_EMPTY)
        public EnumSet<ABC> v;

        public NonEmptyEnumSet(ABC...values) {
            if (values.length == 0) {
                v = EnumSet.noneOf(ABC.class);
            } else {
                v = EnumSet.copyOf(Arrays.asList(values));
            }
        }
    }

    // [databind#5369] Content filtering on collections

    // 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<String> 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<String> items = new LinkedHashSet<>();

        FooSetBean add(String value) {
            items.add(value);
            return this;
        }
    }

    static class NonNullListBean {
        @JsonInclude(content = JsonInclude.Include.NON_NULL)
        public List<String> items = new ArrayList<>();

        NonNullListBean add(String value) {
            items.add(value);
            return this;
        }
    }

    static class NonEmptyListBean {
        @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
        public List<String> items = new ArrayList<>();

        NonEmptyListBean add(String value) {
            items.add(value);
            return this;
        }
    }

    static class NonDefaultListBean {
        @JsonInclude(content = JsonInclude.Include.NON_DEFAULT)
        public List<String> items = new ArrayList<>();

        NonDefaultListBean add(String value) {
            items.add(value);
            return this;
        }
    }

    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<Integer> values = new ArrayList<>();

        IntegerListPojo add(int v) {
            values.add(v);
            return this;
        }
    }

    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<Short> values = new ArrayList<>();

        ShortListPojo add(short v) {
            values.add(v);
            return this;
        }
    }

    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<Byte> values = new ArrayList<>();

        ByteListPojo add(byte v) {
            values.add(v);
            return this;
        }
    }

    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<Long> values = new ArrayList<>();

        LongListPojo add(long v) {
            values.add(v);
            return this;
        }
    }

    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<Double> values = new ArrayList<>();

        DoubleListPojo add(double v) {
            values.add(v);
            return this;
        }
    }

    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<String> 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<Integer> numbers = new ArrayList<>();

        public NumberListBean add(Integer value) {
            numbers.add(value);
            return this;
        }
    }

    static class SimpleList5369Bean {
        public List<String> values = new ArrayList<>();

        SimpleList5369Bean add(String v) {
            values.add(v);
            return this;
        }
    }

    enum Test5369Enum { A, FOO, B }

    static class FooEnum5369Filter {
        @Override
        public boolean equals(Object other) {
            return Test5369Enum.FOO.equals(other);
        }
    }

    static class EnumSet5369Bean {
        @JsonInclude(
                content = JsonInclude.Include.CUSTOM,
                contentFilter = FooEnum5369Filter.class
        )
        public EnumSet<Test5369Enum> values;

        EnumSet5369Bean(EnumSet<Test5369Enum> v) { values = v; }
    }

    static class Iterable5369 implements Iterable<Integer>
    {
        private final List<Integer> values;

        public Iterable5369(Integer... ints) { values = Arrays.asList(ints); }

        @Override
        public Iterator<Integer> iterator() { return values.iterator(); }
    }

    static class BeanWithIterableIncludeNonNull
    {
        @JsonInclude(content = JsonInclude.Include.NON_NULL)
        public Iterable<Integer> values = new Iterable5369(1);
    }

    static class IntegerOnly5369Filter {
        @Override
        public boolean equals(Object other) {
            return !Integer.valueOf(5369).equals(other);
        }
    }

    static class BeanWithIterableCustomInclude
    {
        @JsonInclude(
            content = JsonInclude.Include.CUSTOM,
            contentFilter = IntegerOnly5369Filter.class
        )
        public Iterable<Integer> values = new Iterable5369(1);
    }

    // Wildcard-typed Iterable so that `_elementSerializer` stays null and
    // per-element serializers are resolved dynamically at serialize time.
    static class IterableOfObjects implements Iterable<Object>
    {
        private final List<Object> values;

        IterableOfObjects(Object... items) { values = Arrays.asList(items); }

        @Override
        public Iterator<Object> iterator() { return values.iterator(); }
    }

    static class BeanWithIterableNonEmpty
    {
        @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
        public Iterable<Object> values;

        BeanWithIterableNonEmpty(Object... items) {
            values = new IterableOfObjects(items);
        }
    }

    // [databind#5370]: content filtering must also apply on the typed (polymorphic)
    // serialization path of `StringCollectionSerializer`, like the regular path and
    // sibling `IndexedStringListSerializer` already do.
    static class TypedStringCollectionNonEmpty {
        @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
        public Collection<String> values;

        TypedStringCollectionNonEmpty(Collection<String> v) { values = v; }
    }

    static class TypedStringCollectionNonNull {
        @JsonInclude(content = JsonInclude.Include.NON_NULL)
        public Collection<String> values;

        TypedStringCollectionNonNull(Collection<String> v) { values = v; }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    // [databind#5369]
    private final ObjectMapper MAPPER_CONTAINERS = jsonMapperBuilder()
            .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
            .build();

    // [databind#5370]: default typing forces `serializeWithType` on the String collection
    private final ObjectMapper MAPPER_CONTAINERS_TYPED = jsonMapperBuilder()
            .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
            .activateDefaultTyping(NoCheckSubTypeValidator.instance, DefaultTyping.NON_FINAL)
            .build();

    /*
    /**********************************************************************
    /* Test methods, @JsonInclude(NON_EMPTY) on collection field
    /**********************************************************************
     */

    @Test
    public void testEnumSet() throws Exception
    {
        assertEquals("{}", MAPPER.writeValueAsString(new NonEmptyEnumSet()));
        assertEquals("{\"v\":[\"B\"]}", MAPPER.writeValueAsString(new NonEmptyEnumSet(ABC.B)));
    }

    /*
    /**********************************************************************
    /* Test methods, content filtering on collections [databind#5369]
    /**********************************************************************
     */

    // [databind#5369]
    @Test
    public void testCustomFilterWithList() throws Exception {
        assertEquals(a2q("{'items':['1','2']}"),
                MAPPER_CONTAINERS.writeValueAsString(new FooListBean().add("1").add("foo").add("2")));
    }

    @Test
    public void testNonNullContentInclusion() throws Exception {
        assertEquals(a2q("{'items':['1','2']}"),
                MAPPER_CONTAINERS.writeValueAsString(new NonNullListBean().add("1").add(null).add("2")));
    }

    @Test
    public void testNonEmptyContentInclusion() throws Exception {
        assertEquals(a2q("{'items':['1','2']}"),
                MAPPER_CONTAINERS.writeValueAsString(new NonEmptyListBean().add("1").add("").add("2")));
    }

    @Test
    public void testNonDefaultContentInclusion() throws Exception {
        assertEquals(a2q("{'items':['1','2']}"),
                MAPPER_CONTAINERS.writeValueAsString(new NonDefaultListBean().add("1").add(null).add("2")));
    }

    @Test
    public void testCustomFilterWithSet() throws Exception {
        assertEquals(a2q("{'items':['1','2']}"),
                MAPPER_CONTAINERS.writeValueAsString(new FooSetBean().add("1").add("foo").add("2")));
    }

    @Test
    public void testCustomFilterWithIntegerList() throws Exception {
        assertEquals(a2q("{'values':[1,2]}"),
                MAPPER_CONTAINERS.writeValueAsString(new IntegerListPojo().add(1).add(42).add(2)));
    }

    @Test
    public void testCustomFilterWithShortList() throws Exception {
        assertEquals(a2q("{'values':[1,2]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new ShortListPojo().add((short) 1).add((short) 7).add((short) 2)));
    }

    @Test
    public void testCustomFilterWithByteList() throws Exception {
        assertEquals(a2q("{'values':[1,2]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new ByteListPojo().add((byte) 1).add((byte) 9).add((byte) 2)));
    }

    @Test
    public void testCustomFilterWithDoubleList() throws Exception {
        assertEquals(a2q("{'values':[0.5,2.5]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new DoubleListPojo().add(0.5).add(1.25).add(2.5)));
    }

    @Test
    public void testCustomFilterWithLongList() throws Exception {
        assertEquals(a2q("{'values':[10,20]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new LongListPojo().add(10L).add(100L).add(20L)));
    }

    @Test
    public void testCustomFilterWithNumbers() throws Exception {
        assertEquals(a2q("{'numbers':[1,3]}"),
                MAPPER_CONTAINERS.writeValueAsString(new NumberListBean().add(1).add(42).add(3)));
    }

    @Test
    public void testEmptyListWithCustomFilter() throws Exception {
        assertEquals(a2q("{'items':[]}"),
                MAPPER_CONTAINERS.writeValueAsString(new FooListBean()));
    }

    @Test
    public void testAllFilteredOut() throws Exception {
        assertEquals(a2q("{'items':[]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new FooListBean().add("foo").add("foo").add("foo")));
    }

    @Test
    public void testMixedNullsAndFiltered() throws Exception {
        // Custom filter should not filter nulls (based on FooFilter.equals implementation)
        assertEquals(a2q("{'items':['1',null,'2',null]}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new FooListBean().add("1").add(null).add("foo").add("2").add(null)));
    }

    @Test
    public void testContentIncludeOverrideForCollection() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
                .withConfigOverride(List.class,
                        o -> o.setInclude(JsonInclude.Value.empty().withContentFilter(FooFilter.class)))
                .build();

        assertEquals(a2q("{'values':['1','2']}"),
                mapper.writeValueAsString(new SimpleList5369Bean().add("1").add("foo").add("2")));
    }

    @Test
    public void testContentIncludeOverrideForList() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(SerializationFeature.APPLY_JSON_INCLUDE_FOR_CONTAINERS)
                .withConfigOverride(List.class,
                        o -> o.setInclude(JsonInclude.Value.empty().withContentFilter(FooFilter.class)))
                .build();

        assertEquals(a2q("{'values':['1','2']}"),
                mapper.writeValueAsString(new SimpleList5369Bean().add("1").add("foo").add("2")));
    }

    @Test
    public void testEnumSetWithContentFilter() throws Exception
    {
        assertEquals(a2q("{'values':['A','B']}"),
                MAPPER_CONTAINERS.writeValueAsString(
                        new EnumSet5369Bean(EnumSet.of(Test5369Enum.A, Test5369Enum.FOO, Test5369Enum.B))));
    }

    @Test
    public void testIterableWithContentFilteringForNulls() throws Exception
    {
        BeanWithIterableIncludeNonNull pojo = new BeanWithIterableIncludeNonNull();
        pojo.values = new Iterable5369(1, null, 2, null, 3);

        assertEquals("{\"values\":[1,2,3]}", MAPPER_CONTAINERS.writeValueAsString(pojo));
    }

    @Test
    public void testIterableWithContentFilteringMagicNumber() throws Exception
    {
        BeanWithIterableCustomInclude pojo = new BeanWithIterableCustomInclude();
        pojo.values = new Iterable5369(1, null, 2, 3, 5369);

        assertEquals("{\"values\":[5369]}", MAPPER_CONTAINERS.writeValueAsString(pojo));
    }

    // Regression: when `_elementSerializer` is not resolved statically (wildcard/Object element type),
    // per-element dynamic serializer must be passed to `_shouldSerializeElement` so that
    // `serializer.isEmpty(ctxt, elem)` is consulted for NON_EMPTY content inclusion.
    @Test
    public void testIterableNonEmptyWithWildcardElementType() throws Exception
    {
        BeanWithIterableNonEmpty pojo = new BeanWithIterableNonEmpty(
                Collections.emptyMap(),
                Collections.singletonMap("k", "v"),
                Collections.emptyList(),
                Collections.singletonList("x"));

        assertEquals(a2q("{'values':[{'k':'v'},['x']]}"),
                MAPPER_CONTAINERS.writeValueAsString(pojo));
    }

    // [databind#5370]: NON_EMPTY content filtering applied on typed (polymorphic) path,
    // not just the regular `serialize()` path
    @Test
    public void testTypedStringCollectionNonEmpty() throws Exception
    {
        Collection<String> set = new LinkedHashSet<>(Arrays.asList("1", "", "2"));
        assertEquals(a2q("['tools.jackson.databind.ser.filter.JsonIncludeCollectionTest$TypedStringCollectionNonEmpty',"
                        + "{'values':['java.util.LinkedHashSet',['1','2']]}]"),
                MAPPER_CONTAINERS_TYPED.writeValueAsString(new TypedStringCollectionNonEmpty(set)));
    }

    // [databind#5370]: NON_NULL content filtering applied on typed (polymorphic) path
    @Test
    public void testTypedStringCollectionNonNull() throws Exception
    {
        Collection<String> set = new LinkedHashSet<>(Arrays.asList("1", null, "2"));
        assertEquals(a2q("['tools.jackson.databind.ser.filter.JsonIncludeCollectionTest$TypedStringCollectionNonNull',"
                        + "{'values':['java.util.LinkedHashSet',['1','2']]}]"),
                MAPPER_CONTAINERS_TYPED.writeValueAsString(new TypedStringCollectionNonNull(set)));
    }
}