GenericTypeDeserTest.java

package tools.jackson.databind.deser;

import java.util.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;

import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.*;

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

import static tools.jackson.databind.testutil.DatabindTestUtil.a2q;
import static tools.jackson.databind.testutil.DatabindTestUtil.jsonMapperBuilder;
import static tools.jackson.databind.testutil.DatabindTestUtil.newJsonMapper;

/**
 * Tests for deserialization of generic types: generic collections,
 * generic value wrappers, recursive wildcards, and multi-param wildcards.
 */
public class GenericTypeDeserTest
{
    /*
    /**********************************************************************
    /* Helper classes for generic value tests
    /**********************************************************************
     */

    static abstract class BaseNumberBean<T extends Number>
    {
        public abstract void setNumber(T value);
    }

    static class NumberBean
        extends BaseNumberBean<Long>
    {
        long _number;

        @Override
        public void setNumber(Long value)
        {
            _number = value.intValue();
        }
    }

    static class SimpleBean
    {
        public int x;
    }

    static class Wrapper<T>
    {
        public T value;

        public Wrapper() { }

        public Wrapper(T v) { value = v; }

        @Override
        public boolean equals(Object o) {
            return (o instanceof Wrapper<?>) && (((Wrapper<?>) o).value.equals(value));
        }
    }

    /*
    /**********************************************************************
    /* Helper classes for recursive wildcard tests [databind#4118]
    /**********************************************************************
     */

    static class Tree<T extends Tree<?>> {

        final List<T> children;

        @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
        public Tree(List<T> children) {
            if (!children.stream().allMatch(c -> c instanceof Tree<?>)) {
                throw new IllegalArgumentException("Incorrect type");
            }
            this.children = children;
        }
    }

    static class TestAttribute4118<T extends TestAttribute4118<?>> {

        public List<T> attributes;

        public TestAttribute4118() {
        }

        public TestAttribute4118(List<T> attributes) {
            this.attributes = attributes;
        }
    }

    static class TestObject4118 {

        public List<TestAttribute4118<?>> attributes = new ArrayList<>();

        public TestObject4118() {
        }

        public TestObject4118(List<TestAttribute4118<?>> attributes) {
            this.attributes = attributes;
        }
    }

    /*
    /**********************************************************************
    /* Helper classes for multi-param wildcard tests [databind#4147]
    /**********************************************************************
     */

    // Simulates Either<L, R> pattern
    static class Pair<L, R> {
        public L left;
        public R right;

        public Pair() {}
        public Pair(L left, R right) {
            this.left = left;
            this.right = right;
        }
    }

    static class PairContainer {
        public List<Pair<String, ?>> pairs;

        public PairContainer() {}
        public PairContainer(List<Pair<String, ?>> pairs) {
            this.pairs = pairs;
        }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    /*
    /**********************************************************************
    /* Test methods, generic values/wrappers
    /**********************************************************************
     */

    @Test
    public void testSimpleNumberBean() throws Exception
    {
        NumberBean result = MAPPER.readValue("{\"number\":17}", NumberBean.class);
        assertEquals(17, result._number);
    }

    /**
     * Unit test for verifying fix to [JACKSON-109].
     */
    @Test
    public void testGenericWrapper() throws Exception
    {
        Wrapper<SimpleBean> result = MAPPER.readValue
            ("{\"value\": { \"x\" : 13 } }",
             new TypeReference<Wrapper<SimpleBean>>() { });
        assertNotNull(result);
        assertEquals(Wrapper.class, result.getClass());
        Object contents = result.value;
        assertNotNull(contents);
        assertEquals(SimpleBean.class, contents.getClass());
        SimpleBean bean = (SimpleBean) contents;
        assertEquals(13, bean.x);
    }

    @Test
    public void testGenericWrapperWithSingleElementArray() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();
        Wrapper<SimpleBean> result = mapper.readValue
            ("[{\"value\": [{ \"x\" : 13 }] }]",
             new TypeReference<Wrapper<SimpleBean>>() { });
        assertNotNull(result);
        assertEquals(Wrapper.class, result.getClass());
        Object contents = result.value;
        assertNotNull(contents);
        assertEquals(SimpleBean.class, contents.getClass());
        SimpleBean bean = (SimpleBean) contents;
        assertEquals(13, bean.x);
    }

    // Test for verifying that we can use different
    // type bindings for individual generic types.
    @Test
    public void testMultipleWrappers() throws Exception
    {
        // First, numeric wrapper
        Wrapper<Boolean> result = MAPPER.readValue
            ("{\"value\": true}", new TypeReference<Wrapper<Boolean>>() { });
        assertEquals(new Wrapper<Boolean>(Boolean.TRUE), result);

        // Then string one
        Wrapper<String> result2 = MAPPER.readValue
            ("{\"value\": \"abc\"}", new TypeReference<Wrapper<String>>() { });
        assertEquals(new Wrapper<String>("abc"), result2);

        // And then number
        Wrapper<Long> result3 = MAPPER.readValue
            ("{\"value\": 7}", new TypeReference<Wrapper<Long>>() { });
        assertEquals(new Wrapper<Long>(7L), result3);
    }

    //[databind#381]
    @Test
    public void testMultipleWrappersSingleValueArray() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();

        // First, numeric wrapper
        Wrapper<Boolean> result = mapper.readValue
            ("[{\"value\": [true]}]", new TypeReference<Wrapper<Boolean>>() { });
        assertEquals(new Wrapper<Boolean>(Boolean.TRUE), result);

        // Then string one
        Wrapper<String> result2 = mapper.readValue
            ("[{\"value\": [\"abc\"]}]", new TypeReference<Wrapper<String>>() { });
        assertEquals(new Wrapper<String>("abc"), result2);

        // And then number
        Wrapper<Long> result3 = mapper.readValue
            ("[{\"value\": [7]}]", new TypeReference<Wrapper<Long>>() { });
        assertEquals(new Wrapper<Long>(7L), result3);
    }

    /**
     * Unit test for verifying fix to [JACKSON-109].
     */
    @Test
    public void testArrayOfGenericWrappers() throws Exception
    {
        Wrapper<SimpleBean>[] result = MAPPER.readValue
            ("[ {\"value\": { \"x\" : 9 } } ]",
             new TypeReference<Wrapper<SimpleBean>[]>() { });
        assertNotNull(result);
        assertEquals(Wrapper[].class, result.getClass());
        assertEquals(1, result.length);
        Wrapper<SimpleBean> elem = result[0];
        Object contents = elem.value;
        assertNotNull(contents);
        assertEquals(SimpleBean.class, contents.getClass());
        SimpleBean bean = (SimpleBean) contents;
        assertEquals(9, bean.x);
    }

    // [Issue#381]
    @Test
    public void testArrayOfGenericWrappersSingleValueArray() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
                .build();

        Wrapper<SimpleBean>[] result = mapper.readValue
            ("[ {\"value\": [ { \"x\" : [ 9 ] } ] } ]",
             new TypeReference<Wrapper<SimpleBean>[]>() { });
        assertNotNull(result);
        assertEquals(Wrapper[].class, result.getClass());
        assertEquals(1, result.length);
        Wrapper<SimpleBean> elem = result[0];
        Object contents = elem.value;
        assertNotNull(contents);
        assertEquals(SimpleBean.class, contents.getClass());
        SimpleBean bean = (SimpleBean) contents;
        assertEquals(9, bean.x);
    }

    /*
    /**********************************************************************
    /* Test methods, recursive wildcards [databind#4118]
    /**********************************************************************
     */

    // for [databind#4118]
    @Test
    void recursiveWildcard4118() throws Exception {
        Tree<?> tree = MAPPER.readValue("[[[]]]", new TypeReference<Tree<?>>() {
        });

        assertEquals(1, tree.children.size());
        assertEquals(1, tree.children.get(0).children.size());
        assertEquals(0, tree.children.get(0).children.get(0).children.size());
    }

    // for [databind#4118]
    @Test
    void deserWildcard4118() throws Exception {
        // Given
        TestAttribute4118<?> a = new TestAttribute4118<>(null);
        TestAttribute4118<?> b = new TestAttribute4118<>(Arrays.asList(a));
        TestAttribute4118<?> c = new TestAttribute4118<>(Arrays.asList(b));
        TestObject4118 test = new TestObject4118(Arrays.asList(c));

        String serialized = MAPPER.writeValueAsString(test);

        // When
        TestObject4118 deserialized = MAPPER.readValue(serialized, TestObject4118.class);

        // Then
        assertInstanceOf(TestAttribute4118.class, deserialized.attributes.get(0).attributes.get(0));
    }

    /*
    /**********************************************************************
    /* Test methods, multi-param wildcards [databind#4147]
    /**********************************************************************
     */

    // for [databind#4147]
    @Test
    void multiParamWithWildcard() throws Exception {
        String json = a2q("{'pairs':[{'left':'test','right':123}]}");
        PairContainer result = MAPPER.readValue(json, PairContainer.class);

        assertNotNull(result.pairs);
        assertEquals(1, result.pairs.size());
        assertEquals("test", result.pairs.get(0).left);
        assertNotNull(result.pairs.get(0).right);
        // Right side should be deserialized as Integer (not LinkedHashMap)
        assertEquals(Integer.class, result.pairs.get(0).right.getClass());
        assertEquals(123, result.pairs.get(0).right);
    }

    // Additional test: both parameters are wildcards
    @Test
    void multiParamWithBothWildcards() throws Exception {
        String json = a2q("{'left':'hello','right':456}");

        Pair<?, ?> result = MAPPER.readValue(json, Pair.class);

        assertNotNull(result);
        assertEquals("hello", result.left);
        assertEquals(456, result.right);
    }
}