Skip to content

Error deserializing generic type hierarchy #814

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

Closed
stuartdykes opened this issue Jun 1, 2015 · 6 comments
Closed

Error deserializing generic type hierarchy #814

stuartdykes opened this issue Jun 1, 2015 · 6 comments
Milestone

Comments

@stuartdykes
Copy link

I have a generic parent type:

public abstract class GenericIdentifiableObject<T> {
    private T id;
    ...
}

with two generic subclasses:

public abstract class GenericParent<T extends GenericChild> extends GenericIdentifiableObject<Integer> {
    private List<T> children;
    ...
}

public abstract class GenericChild extends GenericIdentifiableObject<Integer> {
    private String name;
    ...
}

and two further concrete subclasses:

public class ConcreteParent extends GenericParent<ConcreteChild> {
    ...
}

public class ConcreteChild extends GenericChild {
    ...
}

When I try to deserialize an instance of ConcreteParent, I get the following error:

com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class JacksonTest$ConcreteChild] from Integral number (1); no single-int-arg constructor/factory method
 at [Source: [B@8a590d1; line: 1, column: 2] (through reference chain: ConcreteParent["id"])
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
    at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:843)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromInt(StdValueInstantiator.java:304)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromNumber(BeanDeserializerBase.java:1098)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:155)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:144)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:523)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:95)
    at com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap.findDeserializeAndSet(BeanPropertyMap.java:285)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:248)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:136)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3560)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2669)
    at JacksonTest.test(JacksonTest.java:29)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:211)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:67)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

It appears that Jackson tries to deserialize the "id" field on ConcreteParent into an instance of ConcreteChild. I'm guessing it's something to do with the shared generic superclass and the parameterisation of GenericParent with respect to GenericChild.

I've tried to resolve this using @JsonTypeInfo annotations without success. Is there a combination of annotations that might help? Also worth noting, if I change the id field in GenericIdentifiableObject from T to Integer, the problem goes away. Unfortunately I'm not in a position to make this change to the classes I need to deserialize.

The JUnit test case below illustrates the error. This is using Jackson 2.5.0, dependencies as follows:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.5.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.0</version>
</dependency>
import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonTest {

  private ObjectMapper objectMapper = new ObjectMapper();

  @Test
  public void test() throws Exception {

    ConcreteChild child = new ConcreteChild();
    child.setId(101);

    ConcreteParent parent = new ConcreteParent();
    parent.setId(1);
    parent.setChildren(new ArrayList<ConcreteChild>());
    parent.getChildren().add(child);

    String jsonResult = objectMapper.writeValueAsString(parent);

    ConcreteParent result = objectMapper.readValue(jsonResult.getBytes(), ConcreteParent.class);

    assertEquals(result.getId(), parent.getId());
    assertEquals(result.getChildren().size(), parent.getChildren().size());
  }

  public static abstract class GenericIdentifiableObject<T> {

    private T id;

    public T getId() {
      return id;
    }

    public void setId(T id) {
      this.id = id;
    }
  }

  public static abstract class GenericParent<T extends GenericChild> extends GenericIdentifiableObject<Integer> {

    private List<T> children;

    public List<T> getChildren() {
      return children;
    }

    public void setChildren(List<T> children) {
      this.children = children;
    }
  }

  public static abstract class GenericChild extends GenericIdentifiableObject<Integer> {

    private String name;

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }

  public static class ConcreteParent extends GenericParent<ConcreteChild> {

  }

  public static class ConcreteChild extends GenericChild {

  }
}
@cowtowncoder
Copy link
Member

Could you include serialization from line

String jsonResult = objectMapper.writeValueAsString(parent);

since that could give a hint as to what is going on. Exception itself suggests that the serialization of ConcreteChild would be a JSON integral number, so this could be a problem with either serialization or deserialization. With polymorphic handling (@JsonTypeInfo), there would be other complications, but from quick look existing code seems like it ought to work as is.

@stuartdykes
Copy link
Author

The serialization looks like it's working correctly:

{"id":1,"children":[{"id":101,"name":null}]}

which is what I'd expect (unless I'm missing something).

The code looks like it should work to me too. My attempt at using @JsonTypeInfo was a shot in the dark - I don't think we need this as we're not doing polymorphic deserialization, but I thought perhaps it might give a hint about the types involved.

@stuartdykes
Copy link
Author

It appears Jackson is getting confused by the parent and child classes both having a type parameter T. If I change the type parameter from T to S, the deserialization succeeds:

GenericParent<S extends GenericChild>

@RutledgePaulV
Copy link

I'm seeing the same issue. Renaming generic parameterizations so that no two in the same inheritance tree share the same variable name fixed it. I thought I was going mad.

@cowtowncoder
Copy link
Member

Yes, unfortunately there are no known issues related to aliasing of type variables (there are couple of unit tests under failing/ to demonstrate ones we have seen). It would be possible to solve these if Jackson could use java-classmate package, but retrofitting existing type API (JavaType) is a difficult problem.

So I suspect this is due to same root cause as #76.

@cowtowncoder
Copy link
Member

Thanks to reimplementation of generic type resolution, this should now work with 2.7(.0-rc1) without having to use specific type variable name.

@cowtowncoder cowtowncoder added this to the 2.7.0 milestone Dec 12, 2015
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