Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SimpleAbstractTypeResolver breaks generic parameters #1186

Closed
tobiash opened this issue Apr 6, 2016 · 7 comments
Closed

SimpleAbstractTypeResolver breaks generic parameters #1186

tobiash opened this issue Apr 6, 2016 · 7 comments
Milestone

Comments

@tobiash
Copy link

tobiash commented Apr 6, 2016

Upgrading to Jackson 2.7.3 breaks generic parameters for me when using an abstract type mapping.
I'm not sure if this is related to #1173, but I'm not using any raw types here. I was able to create a simple test case that seems very similiar to the examples given in the comments of SimpleAbstractTypeResolver, yet it doesn't work:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.Test;

import java.io.IOException;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class AbstractTypeMappingGenericsProblemTest {

    public interface IContainer<T> {
        @JsonProperty("ts")
        List<T> getTs();
    }

    public static class MyContainer<T> implements IContainer<T> {

        final List<T> ts;

        @JsonCreator
        public MyContainer(@JsonProperty("ts") List<T> ts) {
            this.ts = ts;
        }

        public List<T> getTs() {
            return ts;
        }
    }

    public static class MyObject {
        @JsonProperty("msg")
        String msg;
    }


    @Test
    public void testDeserializeMyContainer() throws IOException {
        Module module = new SimpleModule().addAbstractTypeMapping(IContainer.class, MyContainer.class);
        final ObjectMapper mapper = new ObjectMapper().registerModule(module);
        String json = "{\"ts\": [ { \"msg\": \"hello\"} ] }";
        final Object o = mapper.readValue(json, mapper.getTypeFactory().constructParametricType(IContainer.class, MyObject.class));
        assertThat(o, instanceOf(MyContainer.class));
        assertThat(o, hasProperty("ts", contains(instanceOf(MyObject.class))));
    }

}

I am able to make the test case work using a custom implementation of AbstractTypeResolver that directly resolves to a parameterized type:

        Module module = new Module() {

            // ....

            @Override
            public void setupModule(SetupContext context) {
                context.addAbstractTypeResolver(new AbstractTypeResolver() {
                    @Override
                    public JavaType findTypeMapping(DeserializationConfig config, JavaType type) {
                        //  Just a simple stub implementation
                        if (type.getRawClass().equals(IContainer.class)) {
                            return config.getTypeFactory().constructParametricType(MyContainer.class, MyObject.class);
                        }
                        return super.findTypeMapping(config, type);
                    }
                });
            }
        };

So is this a bug or does SimpleAbstractTypeResolver just not really support generics? Or did I misunderstand something here? Is implementing a custom AbstractTypeResolver the way to go?

@cowtowncoder
Copy link
Member

From quick look your usage seems valid, and there certainly should not be need for custom resolver.
I will try to look into this later on, but how does the test fail here?

@tobiash
Copy link
Author

tobiash commented Apr 7, 2016

Sorry, forgot to mention this: The list contains instances of LinkedHashMap instead of MyObject. This happens only when Jackson has to use AbstractTypeResolver; it works as soon as I request the concrete class:

mapper.readValue(json, mapper.getTypeFactory().constructParametricType(MyContainer.class, MyObject.class)) // List gets filled with MyObject instances 

@oldButNotWise
Copy link

Found a similar problem with deserializing parameterized interfaces:

    private static class MyList {
        @JsonProperty("strings")
        List<String> strings;
        @JsonCreator
        MyList(@JsonProperty("strings") String... strings) {
            this.strings = Arrays.asList(strings);
        }
    } // MyList

    @JsonDeserialize(as = HolderImpl.class)
    private static interface Holder<S> {
        S getData();
    } // Holder

    private static class HolderImpl<S> implements Holder<S> {
        private final S data;
        @JsonCreator
        HolderImpl(@JsonProperty("stuff") S data) {
            this.data = data;
        }
        @JsonProperty("stuff")
        @Override
        public S getData() {
            return data;
        }
    } // HolderImpl

    public boolean isOk() throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        Holder<MyList> h = new HolderImpl<MyList>(new MyList("Hydrogen", "Helium"));
        String json = mapper.writeValueAsString(h);
        Holder<MyList> parsed = mapper.readValue(json, new TypeReference<Holder<MyList>>() { });
        return parsed.getData() instanceof MyList;
    }

Using 2.6.x the isOk() method returns true. Using 2.7.0+ it returns false: the type is not MyList but instead is LinkedHashMap.

A co-worker adds:

In 2.6.3 when the TypeReference and the "as" annotation are brought together, it goes through this method, com.fasterxml.jackson.databind.JavaType#narrowBy() which seems to end up with HolderImpl<MyList>, i.e. type params preserved.

In 2.7.0, in the same general area, com.fasterxml.jackson.databind.JavaType#forcedNarrowBy() (or _narrow()?) is never called at all. Instead, it goes through refine() in com.fasterxml.jackson.databind.type.TypeFactory#constructSpecializedType():

    if (baseType.isInterface()) {
                newType = baseType.refine(subclass, TypeBindings.emptyBindings(), null,
                        new JavaType[] { baseType });
            } else {
                newType = baseType.refine(subclass, TypeBindings.emptyBindings(), baseType,
                        NO_TYPES);
            }
            // Only SimpleType returns null, but if so just resolve regularly
            if (newType == null) {
                // But otherwise gets bit tricky, as we need to partially resolve the type hierarchy
                // (hopefully passing null Class for root is ok)
                newType = _fromClass(null, subclass, TypeBindings.emptyBindings());        
            }

.... which just returns null for SimpleType so it falls through to a fallback that throws away the <MyList> part. :(

@cowtowncoder
Copy link
Member

Ok, I can reproduce this with 2.7, and it is also present in master (pre-2.8.0). Latter is important because one limitation of 2.7 related to narrowing was fixed for master.

I'll see if I can solve this case: I am pretty sure this does not affect (more) common cases of Map and List specialization into well-known types, but only because there are couple of shortcuts taken to support those. But general-purpose type variable resolution is incomplete due to complexity of reliably doing so. 2.6 and earlier had much simpler handling, where these particular cases did work, but it was otherwise limited (so, for example, type parameter names and positions through hierarchy had to match -- usually this is true, but not always). So this is another regression due to type resolution rewrite, but unfortunately one that is not trivially easy to fix.

cowtowncoder added a commit that referenced this issue Apr 13, 2016
@tobiash
Copy link
Author

tobiash commented Apr 13, 2016

Ok. How would a fix work? Implement an AbstractTypeResolver that basically does what my workaround implementation does, but for the general case, tracking generic variables through the hierarchy?

@cowtowncoder
Copy link
Member

@tobiash In general since you know type bindings, you can explicitly construct subtypes using TypeFactory's constructXxx() methods, instead of trying to use narrow/specialize method in TypeFactory.

Full fix will be in TypeFactory; it will have to sort of push type binding variables (placeholders) from new subtype, and then see how they would bound to values that base type has; and in the end construct new type. It's tricky business; I wrote handling for java-classmate once upon a time but it... gets involved.

@cowtowncoder cowtowncoder added this to the 2.7.4 milestone Apr 14, 2016
@cowtowncoder
Copy link
Member

Ok. I figured out a way to fix usage along this common pattern, and although it is not as generic as I wish (in fact it would suffer from reordering of parameters, or partial binding; although not from aliasing-by-name), I think it should go a long way towards fixing regressions from 2.6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants