CollectionDeserializationTest.java

package tools.jackson.databind.deser.jdk;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import tools.jackson.core.*;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.testutil.NoCheckSubTypeValidator;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

import static tools.jackson.databind.testutil.DatabindTestUtil.*;

@SuppressWarnings("serial")
public class CollectionDeserializationTest
{
    enum Key {
        KEY1, KEY2, WHATEVER;
    }

    @JsonDeserialize(using=ListDeserializer.class)
    static class CustomList extends LinkedList<String> { }

    static class ListDeserializer extends StdDeserializer<CustomList>
    {
        public ListDeserializer() { super(CustomList.class); }

        @Override
        public CustomList deserialize(JsonParser jp, DeserializationContext ctxt)
        {
            CustomList result = new CustomList();
            result.add(jp.getString());
            return result;
        }
    }

    static class XBean {
        public int x;

        public XBean() { }
        public XBean(int x) { this.x = x; }
    }

    // [databind#199]
    static class ListAsIterable {
        public Iterable<String> values;
    }

    // [databind#2251]
    static class ListAsAbstract {
        public AbstractList<String> values;
    }

    static class SetAsAbstract {
        public AbstractSet<String> values;
    }

    static class ListAsIterableX {
        public Iterable<XBean> nums;
    }

    static class KeyListBean {
        public List<Key> keys;
    }

    // [databind#828]
    @JsonDeserialize(using=SomeObjectDeserializer.class)
    static class SomeObject {}

    static class SomeObjectDeserializer extends StdDeserializer<SomeObject> {
        public SomeObjectDeserializer() { super(SomeObject.class); }

        @Override
        public SomeObject deserialize(JsonParser p, DeserializationContext ctxt) {
            throw new RuntimeException("I want to catch this exception");
        }
    }

    // [databind#3068]: Exception wrapping (or not)
    static class MyContainerModel {
        @JsonProperty("processor-id")
        public String id = "123";
    }

    static class MyJobModel {
        public Map<String, MyContainerModel> containers = Collections.singletonMap("key",
                new MyContainerModel());
        public int maxChangeLogStreamPartitions = 13;
    }

    static class CustomException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public CustomException(String s) {
            super(s);
        }
    }

    // [databind#5522]
    @JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
    static class CustomNumberList5522 extends ArrayList<Number> { }

    @JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
    static class CustomStringList5522 extends ArrayList<String> { }

    static class CustomClassForNumber5522 {
        private CustomNumberList5522 value;

        public CustomNumberList5522 getValue() {
            return value;
        }
        public void setValue(CustomNumberList5522 value) {
            this.value = value;
        }
    }

    static class CustomClassForString5522 {
        private CustomStringList5522 value;

        public CustomStringList5522 getValue() {
            return value;
        }
        public void setValue(CustomStringList5522 value) {
            this.value = value;
        }
    }

    static class CustomClassForListField5522 {
        @JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
        private List<String> value;

        public List<String> getValue() {
            return value;
        }
        public void setValue(List<String> value) {
            this.value = value;
        }
    }

    /*
    /**********************************************************************
    /* Test methods
    /**********************************************************************
     */

    private final static ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void testUntypedList() throws Exception
    {
        // to get "untyped" default List, pass Object.class
        String JSON = "[ \"text!\", true, null, 23 ]";

        // Not a guaranteed cast theoretically, but will work:
        // (since we know that Jackson will construct an ArrayList here...)
        Object value = MAPPER.readValue(JSON, Object.class);
        assertNotNull(value);
        assertInstanceOf(ArrayList.class, value);
        List<?> result = (List<?>) value;

        assertEquals(4, result.size());

        assertEquals("text!", result.get(0));
        assertEquals(Boolean.TRUE, result.get(1));
        assertNull(result.get(2));
        assertEquals(Integer.valueOf(23), result.get(3));
    }

    @Test
    public void testExactStringCollection() throws Exception
    {
        // to get typing, must use type reference
        String JSON = "[ \"a\", \"b\" ]";
        List<String> result = MAPPER.readValue(JSON, new TypeReference<ArrayList<String>>() { });

        assertNotNull(result);
        assertEquals(ArrayList.class, result.getClass());
        assertEquals(2, result.size());

        assertEquals("a", result.get(0));
        assertEquals("b", result.get(1));
    }

    @Test
    public void testHashSet() throws Exception
    {
        String JSON = "[ \"KEY1\", \"KEY2\" ]";

        EnumSet<Key> result = MAPPER.readValue(JSON, new TypeReference<EnumSet<Key>>() { });
        assertNotNull(result);
        assertTrue(EnumSet.class.isAssignableFrom(result.getClass()));
        assertEquals(2, result.size());

        assertTrue(result.contains(Key.KEY1));
        assertTrue(result.contains(Key.KEY2));
        assertFalse(result.contains(Key.WHATEVER));
    }

    /// Test to verify that @JsonDeserialize.using works as expected
    @Test
    public void testCustomDeserializer() throws IOException
    {
        CustomList result = MAPPER.readValue(q("abc"), CustomList.class);
        assertEquals(1, result.size());
        assertEquals("abc", result.get(0));
    }

    // Testing "implicit JSON array" for single-element arrays,
    // mostly produced by Jettison, Badgerfish conversions (from XML)
    @SuppressWarnings("unchecked")
    @Test
    public void testImplicitArrays() throws Exception
    {
        // can't share mapper, custom configs (could create ObjectWriter tho)
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
                .build();

        // first with simple scalar types (numbers), with collections
        List<Integer> ints = mapper.readValue("4", List.class);
        assertEquals(1, ints.size());
        assertEquals(Integer.valueOf(4), ints.get(0));
        List<String> strings = mapper.readValue(q("abc"), new TypeReference<ArrayList<String>>() { });
        assertEquals(1, strings.size());
        assertEquals("abc", strings.get(0));
        // and arrays:
        int[] intArray = mapper.readValue("-7", int[].class);
        assertEquals(1, intArray.length);
        assertEquals(-7, intArray[0]);
        String[] stringArray = mapper.readValue(q("xyz"), String[].class);
        assertEquals(1, stringArray.length);
        assertEquals("xyz", stringArray[0]);

        // and then with Beans:
        List<XBean> xbeanList = mapper.readValue("{\"x\":4}", new TypeReference<List<XBean>>() { });
        assertEquals(1, xbeanList.size());
        assertEquals(XBean.class, xbeanList.get(0).getClass());

        Object ob = mapper.readValue("{\"x\":29}", XBean[].class);
        XBean[] xbeanArray = (XBean[]) ob;
        assertEquals(1, xbeanArray.length);
        assertEquals(XBean.class, xbeanArray[0].getClass());
    }

    // [JACKSON-620]: allow "" to mean 'null' for Maps
    @Test
    public void testFromEmptyString() throws Exception
    {
        ObjectReader r = MAPPER.reader(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        List<?> result = r.forType(List.class).readValue(q(""));
        assertNull(result);
    }

    // [databind#161]
    @Test
    public void testArrayBlockingQueue() throws Exception
    {
        // ok to skip polymorphic type to get Object
        ArrayBlockingQueue<?> q = MAPPER.readValue("[1, 2, 3]", ArrayBlockingQueue.class);
        assertNotNull(q);
        assertEquals(3, q.size());
        assertEquals(Integer.valueOf(1), q.take());
        assertEquals(Integer.valueOf(2), q.take());
        assertEquals(Integer.valueOf(3), q.take());
    }

    // [databind#199]
    @Test
    public void testIterableWithStrings() throws Exception
    {
        String JSON = "{ \"values\":[\"a\",\"b\"]}";
        ListAsIterable w = MAPPER.readValue(JSON, ListAsIterable.class);
        assertNotNull(w);
        assertNotNull(w.values);
        Iterator<String> it = w.values.iterator();
        assertTrue(it.hasNext());
        assertEquals("a", it.next());
        assertEquals("b", it.next());
        assertFalse(it.hasNext());
    }

    @Test
    public void testIterableWithBeans() throws Exception
    {
        String JSON = "{ \"nums\":[{\"x\":1},{\"x\":2}]}";
        ListAsIterableX w = MAPPER.readValue(JSON, ListAsIterableX.class);
        assertNotNull(w);
        assertNotNull(w.nums);
        Iterator<XBean> it = w.nums.iterator();
        assertTrue(it.hasNext());
        XBean xb = it.next();
        assertNotNull(xb);
        assertEquals(1, xb.x);
        xb = it.next();
        assertEquals(2, xb.x);
        assertFalse(it.hasNext());
    }

    // for [databind#506]
    @Test
    public void testArrayIndexForExceptions1() throws Exception
    {
        try {
            MAPPER.readValue("[ \"KEY2\", false ]", Key[].class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize value of type");
            verifyException(e, "from Boolean value");
            assertEquals(1, e.getPath().size());
            assertEquals(1, e.getPath().get(0).getIndex());
        }
    }

    @Test
    public void testArrayIndexForExceptions2() throws Exception
    {
        try {
            MAPPER.readValue("[ \"xyz\", { } ]", String[].class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize value of type");
            verifyException(e, "from Object value");
            assertEquals(1, e.getPath().size());
            assertEquals(1, e.getPath().get(0).getIndex());
        }
    }

    @Test
    public void testArrayIndexForExceptions3() throws Exception
    {
        try {
            MAPPER.readValue("{\"keys\":[ \"KEY2\", false ]}", KeyListBean.class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize value of type");
            verifyException(e, "from Boolean value");
            assertEquals(2, e.getPath().size());
            // Bean has no index, but has name:
            assertEquals(-1, e.getPath().get(0).getIndex());
            assertEquals("keys", e.getPath().get(0).getPropertyName());

            // and for List, reverse:
            assertEquals(1, e.getPath().get(1).getIndex());
            assertNull(e.getPath().get(1).getPropertyName());
        }
    }

    // for [databind#828]
    @Test
    public void testWrapExceptions() throws Exception
    {
        final ObjectReader wrappingReader = MAPPER
                .readerFor(new TypeReference<List<SomeObject>>() {})
                .with(DeserializationFeature.WRAP_EXCEPTIONS);

        try {
            wrappingReader.readValue("[{}]");
        } catch (JacksonException exc) {
            assertEquals("I want to catch this exception", exc.getOriginalMessage());
        } catch (RuntimeException exc) {
            fail("The RuntimeException should have been wrapped with a DatabindException, got: "+exc.getClass());
        }

        final ObjectReader noWrapReader = MAPPER
                .readerFor(new TypeReference<List<SomeObject>>() {})
                .without(DeserializationFeature.WRAP_EXCEPTIONS);

        try {
            noWrapReader.readValue("[{}]");
        } catch (DatabindException exc) {
            fail("It should not have wrapped the RuntimeException.");
        } catch (RuntimeException exc) {
            assertEquals("I want to catch this exception", exc.getMessage());
        }
    }

    // [databind#2251]
    @Test
    public void testAbstractListAndSet() throws Exception
    {
        final String JSON = "{\"values\":[\"foo\", \"bar\"]}";

        ListAsAbstract list = MAPPER.readValue(JSON, ListAsAbstract.class);
        assertEquals(2, list.values.size());
        assertEquals(ArrayList.class, list.values.getClass());

        SetAsAbstract set = MAPPER.readValue(JSON, SetAsAbstract.class);
        assertEquals(2, set.values.size());
        assertEquals(HashSet.class, set.values.getClass());
    }

    // for [databind#3068]
    @Test
    public void testWrapExceptions3068() throws Exception
    {
        final SimpleModule module = new SimpleModule("SimpleModule", Version.unknownVersion())
                .addDeserializer(MyContainerModel.class,
                        new ValueDeserializer<MyContainerModel>() {
                    @Override
                    public MyContainerModel deserialize(JsonParser p, DeserializationContext ctxt) {
                        throw new CustomException("Custom message");
                    }
                });

        final ObjectMapper mapper = jsonMapperBuilder()
                .addModule(module)
                .build();
        final String json = mapper.writeValueAsString(new MyJobModel());

        // First, verify NO wrapping:
        try {
            mapper.readerFor(MyJobModel.class)
                .without(DeserializationFeature.WRAP_EXCEPTIONS)
                .readValue(json);
            fail("Should not pass");
        } catch (CustomException e) {
            verifyException(e, "Custom message");
        } catch (JacksonException e) {
            fail("Should not have wrapped exception, got: "+e);
        }

        // and then wrapping
        try {
            mapper.readerFor(MyJobModel.class)
                .with(DeserializationFeature.WRAP_EXCEPTIONS)
                .readValue(json);
            fail("Should not pass");
        } catch (JacksonException e) {
            verifyException(e, "Custom message");
            Throwable rootC = e.getCause();
            assertNotNull(rootC);
            assertEquals(CustomException.class, rootC.getClass());
        }
    }

    /*
    /**********************************************************
    /* Test methods, JDK Collections [databind#1868], [databind#4262]
    /**********************************************************
     */

    // Round-trip test for singleton collections
    @Test
    public void testSingletonCollections() throws Exception
    {
        final TypeReference<List<XBean>> xbeanListType = new TypeReference<List<XBean>>() { };

        String json = MAPPER.writeValueAsString(Collections.singleton(new XBean(3)));
        Collection<XBean> result = MAPPER.readValue(json, xbeanListType);
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(3, result.iterator().next().x);

        json = MAPPER.writeValueAsString(Collections.singletonList(new XBean(28)));
        result = MAPPER.readValue(json, xbeanListType);
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(28, result.iterator().next().x);
    }

    // [databind#1868]: Verify class name serialized as is
    @Test
    public void testUnmodifiableSet() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .activateDefaultTyping(NoCheckSubTypeValidator.instance,
                        DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
                .build();
        Set<String> theSet = Collections.unmodifiableSet(Collections.singleton("a"));
        String json = mapper.writeValueAsString(theSet);

        assertEquals("[\"java.util.Collections$UnmodifiableSet\",[\"a\"]]", json);

        Set<?> result = mapper.readValue(json, Set.class);
        assertNotNull(result);
        assertEquals(1, result.size());
    }

    // [databind#4262]: Handle problem of `null`s for `TreeSet`
    @Test
    public void testNullsWithTreeSet() throws Exception
    {
        try {
            MAPPER.readValue("[ \"acb\", null, 123 ]", TreeSet.class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "`java.util.Collection` of type ");
            verifyException(e, " does not accept `null` values");
        }
    }

    // for [databind#216]
    @Test
    public void testJava6Types() throws Exception
    {
        Deque<?> dq = MAPPER.readValue("[1]", Deque.class);
        assertNotNull(dq);
        assertEquals(1, dq.size());
        assertInstanceOf(Deque.class, dq);

        NavigableSet<?> ns = MAPPER.readValue("[ true ]", NavigableSet.class);
        assertEquals(1, ns.size());
        assertInstanceOf(NavigableSet.class, ns);
    }

    /*
    /**********************************************************
    /* Test methods, [databind#5522]
    /**********************************************************
     */

    // [databind#5522]
    @Test
    public void testCustomNumberCollectionDeserialize5522() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
            .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .build();

        String jsonValue = """
            {
                "value": 1
            }
            """;

        CustomClassForNumber5522 result = mapper.readValue(jsonValue, CustomClassForNumber5522.class);

        assertThat(result.value)
            .hasSize(1)
            .containsExactly(1);
    }

    // [databind#5522]
    @Test
    public void testCustomStringCollectionDeserialize5522() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
            .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .build();

        String jsonValue = """
            {
                "value": "test"
            }
            """;

        CustomClassForString5522 result = mapper.readValue(jsonValue, CustomClassForString5522.class);

        assertThat(result.value)
            .hasSize(1)
            .containsExactly("test");
    }

    // [databind#5522]
    @Test
    public void testStringCollectionDeserializeInField5522() throws Exception {
        ObjectMapper mapper = jsonMapperBuilder()
            .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
            .build();

        String jsonValue = """
            {
                "value": "test"
            }
            """;

        CustomClassForListField5522 result = mapper.readValue(jsonValue, CustomClassForListField5522.class);

        assertThat(result.value)
            .hasSize(1)
            .containsExactly("test");
    }
}