DefaultTypingWithJsonDeserializeAsTest.java

package tools.jackson.databind.jsontype.deftyping;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.testutil.DatabindTestUtil;
import tools.jackson.databind.testutil.NoCheckSubTypeValidator;

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

/**
 * A type deserializer for a property must be built for the type the property is deserialized
 * as, after `@JsonDeserialize(as/keyAs/contentAs)` has refined the declared type, or the type
 * resolved from a type id is specialized from the declared type instead and the refinement
 * is lost. But whether type ids are expected at all is decided by the declared type, same as
 * on serialization.
 */
public class DefaultTypingWithJsonDeserializeAsTest
    extends DatabindTestUtil
{
    static class IntKeyMapHolder {
        @JsonDeserialize(keyAs = Integer.class)
        public Map<Object, String> map;
    }

    static class IntContentListHolder {
        @JsonDeserialize(contentAs = Integer.class)
        public List<Object> list;
    }

    static class IntContentMapHolder {
        @JsonDeserialize(contentAs = Integer.class)
        public Map<String, Object> map;
    }

    static class LongContentListHolder {
        @JsonDeserialize(contentAs = Long.class)
        public List<Object> list;
    }

    static class IntKeyMapCreatorHolder {
        public final Map<Object, String> map;

        @JsonCreator
        public IntKeyMapCreatorHolder(@JsonProperty("map") @JsonDeserialize(keyAs = Integer.class) Map<Object, String> map) {
            this.map = map;
        }
    }

    static class CustomMap<K, V> extends HashMap<K, V> { }

    static abstract class Base {
        public int x;
    }

    static class Impl extends Base { }

    static class AbstractAsConcreteHolder {
        @JsonDeserialize(as = Impl.class)
        public Base value;
    }

    static class ObjectAsConcreteHolder {
        @JsonDeserialize(as = Impl.class)
        public Object value;
    }

    static class ContentAsConcreteHolder {
        @JsonDeserialize(contentAs = Impl.class)
        public List<Base> list;
    }

    static class ContentAsConcreteAtomicRefHolder {
        @JsonDeserialize(contentAs = Impl.class)
        public AtomicReference<Base> ref;
    }

    static class ContentAsConcreteOptionalHolder {
        @JsonDeserialize(contentAs = Impl.class)
        public Optional<Base> opt;
    }

    private ObjectMapper mapperWith(DefaultTyping typing, JsonTypeInfo.As as) {
        return jsonMapperBuilder()
                .activateDefaultTyping(NoCheckSubTypeValidator.instance, typing, as)
                .build();
    }

    private static final DefaultTyping[] TYPINGS = {
        DefaultTyping.NON_FINAL, DefaultTyping.OBJECT_AND_NON_CONCRETE
    };
    private static final JsonTypeInfo.As[] INCLUSIONS = {
        JsonTypeInfo.As.WRAPPER_ARRAY, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.As.WRAPPER_OBJECT
    };

    @Test
    public void keyAsWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                IntKeyMapHolder holder = new IntKeyMapHolder();
                holder.map = new HashMap<>();
                holder.map.put(1, "a");
                String json = mapper.writeValueAsString(holder);

                IntKeyMapHolder result = mapper.readValue(json, IntKeyMapHolder.class);
                assertEquals(1, result.map.size(), json);
                Object key = result.map.keySet().iterator().next();
                assertEquals(Integer.class, key.getClass(), json);
                assertEquals("a", result.map.get(1), json);
            }
        }
    }

    // Map types other than the few `TypeFactory` has short-cuts for (like `HashMap`) are
    // specialized from type bindings, not from (refined) key type
    @Test
    public void keyAsWithDefaultTypingForOtherMapTypes()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                IntKeyMapHolder holder = new IntKeyMapHolder();

                holder.map = new ConcurrentHashMap<>(Map.of(1, "a"));
                String json = mapper.writeValueAsString(holder);
                IntKeyMapHolder result = mapper.readValue(json, IntKeyMapHolder.class);
                assertEquals(ConcurrentHashMap.class, result.map.getClass(), json);
                assertEquals(Integer.class, result.map.keySet().iterator().next().getClass(), json);

                CustomMap<Object, String> custom = new CustomMap<>();
                custom.put(1, "a");
                holder.map = custom;
                json = mapper.writeValueAsString(holder);
                result = mapper.readValue(json, IntKeyMapHolder.class);
                assertEquals(CustomMap.class, result.map.getClass(), json);
                assertEquals(Integer.class, result.map.keySet().iterator().next().getClass(), json);
            }
        }
    }

    @Test
    public void keyAsOnCreatorWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                Map<Object, String> map = new HashMap<>();
                map.put(1, "a");
                String json = mapper.writeValueAsString(new IntKeyMapCreatorHolder(map));

                IntKeyMapCreatorHolder result = mapper.readValue(json, IntKeyMapCreatorHolder.class);
                Object key = result.map.keySet().iterator().next();
                assertEquals(Integer.class, key.getClass(), json);
            }
        }
    }

    // Integer values are written without type id ("natural" type)
    @Test
    public void contentAsWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);

                IntContentListHolder listHolder = new IntContentListHolder();
                listHolder.list = new ArrayList<>(Arrays.asList(1, 2));
                String json = mapper.writeValueAsString(listHolder);
                IntContentListHolder listResult = mapper.readValue(json, IntContentListHolder.class);
                assertEquals(Arrays.asList(1, 2), listResult.list, json);

                IntContentMapHolder mapHolder = new IntContentMapHolder();
                mapHolder.map = new HashMap<>();
                mapHolder.map.put("a", 1);
                json = mapper.writeValueAsString(mapHolder);
                IntContentMapHolder mapResult = mapper.readValue(json, IntContentMapHolder.class);
                assertEquals(Integer.valueOf(1), mapResult.map.get("a"), json);
            }
        }
    }

    // Long is not a "natural" type: elements are written with type ids (for declared `Object`
    // content) that must still be read, even though Default Typing does not apply to `Long`
    @Test
    public void contentAsLongWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                LongContentListHolder holder = new LongContentListHolder();
                holder.list = new ArrayList<>(Arrays.asList(1L, 2L));
                String json = mapper.writeValueAsString(holder);

                LongContentListHolder result = mapper.readValue(json, LongContentListHolder.class);
                assertEquals(Arrays.asList(1L, 2L), result.list, json);
            }
        }
    }

    // Narrowing to a concrete type must not drop type ids that are written for the
    // declared (abstract, or `Object`) type
    @Test
    public void asConcreteForAbstractWithDefaultTyping()
    {
        for (DefaultTyping typing : new DefaultTyping[] { DefaultTyping.NON_FINAL,
                DefaultTyping.OBJECT_AND_NON_CONCRETE, DefaultTyping.NON_CONCRETE_AND_ARRAYS }) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                AbstractAsConcreteHolder holder = new AbstractAsConcreteHolder();
                Impl impl = new Impl();
                impl.x = 3;
                holder.value = impl;
                String json = mapper.writeValueAsString(holder);

                AbstractAsConcreteHolder result = mapper.readValue(json, AbstractAsConcreteHolder.class);
                assertEquals(Impl.class, result.value.getClass(), json);
                assertEquals(3, result.value.x, json);
            }
        }
    }

    @Test
    public void asConcreteForObjectWithDefaultTyping()
    {
        for (DefaultTyping typing : new DefaultTyping[] { DefaultTyping.NON_FINAL,
                DefaultTyping.OBJECT_AND_NON_CONCRETE, DefaultTyping.JAVA_LANG_OBJECT }) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                ObjectAsConcreteHolder holder = new ObjectAsConcreteHolder();
                Impl impl = new Impl();
                impl.x = 3;
                holder.value = impl;
                String json = mapper.writeValueAsString(holder);

                ObjectAsConcreteHolder result = mapper.readValue(json, ObjectAsConcreteHolder.class);
                assertEquals(Impl.class, result.value.getClass(), json);
                assertEquals(3, ((Impl) result.value).x, json);
            }
        }
    }

    // Reference types go through the same content type deserializer handling as containers
    @Test
    public void contentAsConcreteForReferenceWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                Impl impl = new Impl();
                impl.x = 3;

                ContentAsConcreteAtomicRefHolder refHolder = new ContentAsConcreteAtomicRefHolder();
                refHolder.ref = new AtomicReference<>(impl);
                String json = mapper.writeValueAsString(refHolder);
                ContentAsConcreteAtomicRefHolder refResult = mapper.readValue(json,
                        ContentAsConcreteAtomicRefHolder.class);
                assertEquals(Impl.class, refResult.ref.get().getClass(), json);
                assertEquals(3, refResult.ref.get().x, json);

                ContentAsConcreteOptionalHolder optHolder = new ContentAsConcreteOptionalHolder();
                optHolder.opt = Optional.of(impl);
                json = mapper.writeValueAsString(optHolder);
                ContentAsConcreteOptionalHolder optResult = mapper.readValue(json,
                        ContentAsConcreteOptionalHolder.class);
                assertEquals(Impl.class, optResult.opt.get().getClass(), json);
                assertEquals(3, optResult.opt.get().x, json);
            }
        }
    }

    @Test
    public void contentAsConcreteForAbstractWithDefaultTyping()
    {
        for (DefaultTyping typing : TYPINGS) {
            for (JsonTypeInfo.As as : INCLUSIONS) {
                ObjectMapper mapper = mapperWith(typing, as);
                ContentAsConcreteHolder holder = new ContentAsConcreteHolder();
                Impl impl = new Impl();
                impl.x = 3;
                holder.list = new ArrayList<>(List.of(impl));
                String json = mapper.writeValueAsString(holder);

                ContentAsConcreteHolder result = mapper.readValue(json, ContentAsConcreteHolder.class);
                assertEquals(1, result.list.size(), json);
                assertEquals(Impl.class, result.list.get(0).getClass(), json);
                assertEquals(3, result.list.get(0).x, json);
            }
        }
    }
}