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

Deserialization Not Working Right with Generic Types and Builders #921

Closed
gilbode opened this issue Sep 8, 2015 · 28 comments
Closed

Deserialization Not Working Right with Generic Types and Builders #921

gilbode opened this issue Sep 8, 2015 · 28 comments
Milestone

Comments

@gilbode
Copy link

gilbode commented Sep 8, 2015

When trying to deserialize a generic type using a builder it is deserializing the generic type as a LinkedHashMap instead of the proper type, exact same code deserialized using @JsonCreator works fine. There seems to be something missing in the builder code.
Test case below demonstrates the problem:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import org.junit.Test;

import java.util.List;

public class JacksonDeserTest {
  public static class MyPOJO {
    public String x;
    public String y;

    @JsonCreator
    public MyPOJO(@JsonProperty("x") String x, @JsonProperty("y") String y) {
      this.x = x;
      this.y = y;
    }
  }

  @JsonDeserialize(builder = MyGenericPOJO.Builder.class)
  public static class MyGenericPOJO<T> {
    private List<T> data;

    private MyGenericPOJO(List<T> data) {
      this.data = data;
    }

    public List<T> getData() {
      return data;
    }

    public static class Builder<T> {
      private List<T> data;

      public Builder<T> withData(List<T> data) {
        this.data = data;
        return this;
      }

      public MyGenericPOJO<T> build() {
        return new MyGenericPOJO<T>(data);
      }
    }
  }

  public static class MyGenericPOJOWithCreator<T> {
    private List<T> data;

    private MyGenericPOJOWithCreator(List<T> data) {
      this.data = data;
    }

    @JsonCreator
    public static <T> MyGenericPOJOWithCreator<T> create(@JsonProperty("data") List<T> data) {
      return new MyGenericPOJOWithCreator.Builder<T>().withData(data).build();
    }

    public List<T> getData() {
      return data;
    }

    public static class Builder<T> {
      private List<T> data;

      public Builder<T> withData(List<T> data) {
        this.data = data;
        return this;
      }

      public MyGenericPOJOWithCreator<T> build() {
        return new MyGenericPOJOWithCreator<T>(data);
      }
    }
  }

  @Test
  public void testWithBuilder() throws Exception {
    final ObjectMapper mapper = new ObjectMapper();
    final String json = "{ \"data\": [ { \"x\": \"x\", \"y\": \"y\" } ] }";
    final MyGenericPOJO<MyPOJO> deserialized =
        mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO>>() {});
    assertThat(deserialized.data, hasSize(1));
    assertThat(deserialized.data.get(0), instanceOf(MyPOJO.class));

  }

  @Test
  public void testWithCreator() throws Exception {
    final ObjectMapper mapper = new ObjectMapper();
    final String json = "{ \"data\": [ { \"x\": \"x\", \"y\": \"y\" } ] }";
    final MyGenericPOJOWithCreator<MyPOJO> deserialized =
        mapper.readValue(json, new TypeReference<MyGenericPOJOWithCreator<MyPOJO>>() {});
    assertThat(deserialized.data, hasSize(1));
    assertThat(deserialized.data.get(0), instanceOf(MyPOJO.class));

  }
}
@cowtowncoder
Copy link
Member

Getting a LinkedHashMap does indeed suggest that generic type is dropped, and java.lang.Object is assumed as the type.
Thank you for the test case: I hope this can be supported as expected.

@jaroslawZawila
Copy link

I do face the same problem what is the status of this issue ?? is there any work around??

@cowtowncoder
Copy link
Member

@jaroslawZawila I don't think anyone has been working on this issue.

@jaroslawZawila
Copy link

Thanks for your response. Could you point me in the direction where do you think is a problem with that?? I may look at this as I really need it to work :D

@cowtowncoder
Copy link
Member

@jaroslawZawila I would first verify that this still occurs with 2.7 (that is, 2.7.0-SNAPSHOT from master branch, using local build or snapshot). If it does not occur, you might want to wait for 2.7.0-rc1 which should be released quite soon (I am hoping for next weekend). If it still occurs, the way generics are resolved probably needs more work; you'd have to follow the build process and compare that to standard handling of Bean(De)Serializer. I suspect this is related to either using raw (type erased) type where generic type is needed; or generic type being resolved using wrong context. Latter becomes bit involved, but basically one has to get type from accessor and resolve it against exact class in which it was declared. With 2.7 this is properly automated so that calling 'getType()' of accessor (like AnnotatedField or AnnotatedMethod) should do it all; but with older versions, calls had to do more work-arounds.

So yeah, it can be bit involved. I hope to get this working for 2.7 regardless, but there is a significant backlog of issues to work on unfortunately. So I appreciate any help you can give!

@jaroslawZawila
Copy link

I have tried using 2.7.0-SNAPSHOT unfortunately the issue still exists. I will investigate a bit more.

cowtowncoder added a commit that referenced this issue Dec 1, 2015
@cowtowncoder
Copy link
Member

I can reproduce this with 2.7.0-rc1; added the failing unit test.

vjkoskela pushed a commit to ArpNetworking/commons that referenced this issue Sep 1, 2016
…ic test fails due to a bug in Jackson - FasterXML/jackson-databind#921 - and is currently disabled.
BrandonArp pushed a commit to ArpNetworking/commons that referenced this issue Sep 1, 2016
…ic test fails due to a bug in Jackson - FasterXML/jackson-databind#921 - and is currently disabled. (#16)
@vjkoskela
Copy link
Contributor

I did some checking and the sample test case provided by @gilbode and committed in 53fb51f by @cowtowncoder fails for the heads of all the 2.x branches (2.1 through 2.7 -- inclusive). It also fails on master. So this does not appear to be a regression -- not sure if that was clear, at least based on a-k-g's issue #1210.

I've been debugging this in the 2.7 branch and in BeanDeserializerFactory's createBuilderBasedDeserializer method the valueType contains the bindings as expected, they are not in beanDesc but looking at the BasicBeanDescription that does not seem unexpected. However, the builderType created in this method does not discover/inherit the bindings from the valueType. As a crude first attempt I tried to build the builder type with the value type's bindings:

JavaType builderType = ctxt.getTypeFactory().constructType(builderClass, valueType.getBindings())

However, builderClass is not a ParameterizedType so TypeFactory refuses to perform this construction. Temporarily working around this by exposing _fromClass on TypeFactory publicly yields:

JavaType builderType = ctxt.getTypeFactory()._fromClass(null, builderClass, valueType.getBindings());

And this fixes the problem! -- and all existing tests continue to pass -- So aside from coming up with a cleaner way in TypeFactory to allow this combined type creation (builder base class and value type bindings) there is the question of what assumptions this introduces between the type bindings of the value type and builder.

Consider:

  1. Rename the T parameter to U in the builder only. This results in the same problem where the generic member's type cannot be determined correctly. Below is the pojo, builder and test:
@JsonDeserialize(builder = MyGenericPOJO_B.Builder.class)
    public static class MyGenericPOJO_B<T> {
        private List<T> data;

        private MyGenericPOJO_B(List<T> d) {
            data = d;
        }

        public List<T> getData() {
            return data;
        }

        public static class Builder<U> {
            private List<U> data;

            public Builder<U> withData(List<U> d) {
                data = d;
                return this;
            }

            public MyGenericPOJO_B<U> build() {
                return new MyGenericPOJO_B<U>(data);
            }
        }
    }

    public void testWithBuilder_B() throws Exception {
        final ObjectMapper mapper = new ObjectMapper();
        final String json = "{ \"data\": [ { \"x\": \"x\", \"y\": \"y\" } ] }";
        final MyGenericPOJO_B<MyPOJO> deserialized =
                mapper.readValue(json, new TypeReference<MyGenericPOJO_B<MyPOJO>>() {});
        assertEquals(1, deserialized.data.size());
        Object ob = deserialized.data.get(0);
        assertNotNull(ob);
        assertEquals(MyPOJO.class, ob.getClass());
    }
  1. Two parameters if the order of types are swapped across the value type and builder type. No surprise here either, this fails to deserialize correctly. Below is the pojo, builder and test:
    public static class MyGenericPOJOWithCreator<T, U> {
      private List<T> data;
      private Map<String, U> map;

      private MyGenericPOJOWithCreator(List<T> d, Map<String, U> m) {
          data = d;
          map = m;
      }

      @JsonCreator
      public static <T, U> MyGenericPOJOWithCreator<T, U> create(
              @JsonProperty("data") List<T> data,
              @JsonProperty("map") Map<String, U> map) {
          return new MyGenericPOJOWithCreator.Builder<T, U>().withData(data).withMap(map).build();
      }

      public List<T> getData() {
          return data;
      }

      public Map<String, U> getMap() {
          return map;
      }

      public static class Builder<U, T> {
          private List<U> data;
          private Map<String, T> map;

          public Builder<U, T> withData(List<U> d) {
              data = d;
              return this;
          }

          public Builder<U, T> withMap(Map<String, T> m) {
              map = m;
              return this;
          }

          public MyGenericPOJOWithCreator<U, T> build() {
              return new MyGenericPOJOWithCreator<U, T>(data, map);
          }
      }
    }

    public void testWithBuilder() throws Exception {
      final ObjectMapper mapper = new ObjectMapper();
      final String json = "{ \"data\": [ { \"x\": \"x\", \"y\": \"y\" } ], \"map\": { \"a\": 1, \"b\": 2 } }";
      final MyGenericPOJO<MyPOJO, Integer> deserialized =
          mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO, Integer>>() {});
      assertEquals(1, deserialized.data.size());
      Object ob = deserialized.data.get(0);
      assertNotNull(ob);
      assertEquals(MyPOJO.class, ob.getClass());
    }

The exception shows it tries to deserialize the integer value for the data object:

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.Integer out of START_OBJECT token
 at [Source: { "data": [ { "x": "x", "y": "y" } ], "map": { "a": 1, "b": 2 } }; line: 1, column: 13] (through reference chain: com.fasterxml.jackson.databind.Builder["data"]->java.util.ArrayList[0])

The type reference for the value type does not imply anything about the builder's generic type bindings. One option that we could consider a BuilderTypeReference to pass such a binding into the deserialization. Another option would be to add annotations to the builder and/or value expressing how the bindings map from the value class to the builder class. However, such measures should only be necessary when the bindings don't map 1:1 in name and order across the value type and builder type. The common case for us at least is that the parameter mapping exists so simply supporting transfer of bindings does not seem like an unreasonable start.

If someone can give some guidance on my strategy and how TypeFactory should be extended to supporting merging a value type's bindings with the builder class into a builder type I can submit a PR for this. Otherwise, if I'm barking up the wrong tree here any pointers would be welcome!

@olasundell
Copy link

I've searched in vain for a +1 feature on GitHub issues but to no avail. Here goes: I just ran into this issue. Can't tell whether it's a bug or a WAD waiting for improvement. I strongly support fixing this.

@cowtowncoder
Copy link
Member

Ok. After re-reading everything above I think I finally understand the problem accurately. Many thanks to @vjkoskela for digging through it. That's good news.

As to bad news... to be completely honest I don't know if this will ever be supported. @vjkoskela is correct in pointing out that the issue is that builder type is specified as type-erased class and there is no way to really either indicate or infer type parameterization. It is not safe, in general, to assume that there is strict equivalency of bindings between value and builder type declarations: even if names are same (like T) seems dangerous. As per points (1) and (2) these wouldn't work.

In theory one could perhaps figure out type by first fully resolving value type, with generics; then locate build method from builder, and try to backtrack actual type parameterization by matching expected value type into type parameters exposed by build method. This is probably solvable but would require someone to really really dig deep into type handling (reification?). I don't think I'll have time or interest to pursue that myself.

Having said that, maybe naive matching of type bindings between value and builder could be acceptable, but only if:

  1. Number of names of type variables are identical. So, <T> would match <T>, but not <U> or <T,V>
  2. Bounds should be compatible (perhaps even strictly base bound)

and if above do not match, a hard exception should be thrown to indicate invalid definitions.
This could achieve two goals:

  1. Work for simplest/trivial cases, and should allow translation of other cases to these (that is, avoiding use of same id etc)
  2. Catch many/most problems early, instead of failing in more mysterious ways (like when bound types do not match).

@spharris
Copy link

spharris commented Dec 4, 2016

A possible workaround:

Add a factory method with @JsonCreator and then add the builder anyway.

@alicederyn
Copy link

Why not make the dumb assumption, check the build method returns the correct type given that assumption, and error out if anything looks wrong?

@cowtowncoder
Copy link
Member

@ChrisAlice At high level, yes, that would seem sensible, but the problem comes from possible complexity of getting there: if we were talking about non-generic types, yes, easy enough to check.
But with generics it is necessary to handle the whole type resolution synchronized to use type bindings from one to the other. Check itself could probably only check raw (type-erased, non generic) return type.

Still, such a setup could be doable and sufficient, worth trying out if anyone has time to work on this.

@vjkoskela
Copy link
Contributor

Ran into this again and this time the generic builders are nested so my previous work around of just deserializing into the builder and building by hand does not work.

The PR #1796 addresses only the common case of when the builder type bindings can be exactly inferred from the value type's bindings. This covers all of our use cases, but does not present a complete solution; for this reason, the functionality is controlled in the PR by a MapperFeature and disabled by default.

Happy to make any changes you require, but I would love a general thumbs up/down as soon as possible so I know whether we should rely on this behavior (I can run a fork for months if need be while we work through the MR and release).

@spharris can you elaborate on your work around?

@spharris
Copy link

I see now that it was just mentioned in the original bug: create a builder that Jackson doesn't know about and also add a constructor annotated with @JsonCreator. The annotation can also go on the constructor itself rather than an additional create method. It's (approximately) the same amount of code if you're making immutable POJOs. Here's the example from op:

public static class MyGenericPOJOWithCreator<T> {
  private List<T> data;

  private MyGenericPOJOWithCreator(List<T> data) {
    this.data = data;
  }

  @JsonCreator
  public static <T> MyGenericPOJOWithCreator<T> create(@JsonProperty("data") List<T> data) {
    return new MyGenericPOJOWithCreator.Builder<T>().withData(data).build();
  }

  public List<T> getData() {
    return data;
  }

  public static class Builder<T> {
    private List<T> data;

    public Builder<T> withData(List<T> data) {
      this.data = data;
      return this;
    }

    public MyGenericPOJOWithCreator<T> build() {
      return new MyGenericPOJOWithCreator<T>(data);
    }
  }
}

@vjkoskela
Copy link
Contributor

Thanks @spharris !

I took the example in the tests and expanded it a little to see how it behaved (modified code at the end). I have not nailed down all the semantics of the generic type mapping but here are some preliminary results.

  1. When the JsonCreator uses the same generic parameters they must be in the same order.

Works:

      @JsonCreator
      public static <T, U> MyGenericPOJOWithCreator<T, U> create(
              @JsonProperty("dataT") List<T> dataT,
              @JsonProperty("dataU") List<U> dataU) {
          return new MyGenericPOJOWithCreator.Builder<T, U>().withDataT(dataT).withDataU(dataU).build();
      }

Does not Work:

      @JsonCreator
      public static <U, T> MyGenericPOJOWithCreator<U, T> create(
              @JsonProperty("dataT") List<U> dataT,
              @JsonProperty("dataU") List<T> dataU) {
          return new MyGenericPOJOWithCreator.Builder<U, T>().withDataT(dataT).withDataU(dataU).build();
      }
  1. If you use completely unrelated generic parameters, say X and Y, the deserialization also fails. The POJO is returned as a LinkedHashMap instead of as an instance of the POJO. This implies to me that JsonCreator has a similar limitation to the Builder approach.
      @JsonCreator
      public static <X, Y> MyGenericPOJOWithCreator<X, Y> create(
              @JsonProperty("dataT") List<X> dataT,
              @JsonProperty("dataU") List<Y> dataU) {
          return new MyGenericPOJOWithCreator.Builder<X, Y>().withDataT(dataT).withDataU(dataU).build();
      }

The failure seems to occur regardless of whether the pojo itself was parameterized (e.g. MyPOJO) or if MyPOJO were not parameterized at all.

The conclusion I am drawing so far is that @JsonCreator deserialization already does generic type inference between the creator method parameters and the provided type reference. Therefore, it seems less unreasonable to have the Builder pattern do the same thing.

I have not found where in the data bind code this path infers the generic parameters from the type reference. Any help either here or with understanding my conclusion would be appreciated (@cowtowncoder ?) .

If my conclusion is correct, then in my opinion it follows that the MR #1796 probably should be the default behavior -- but I'm open either way -- as it seems to bring builder based deserialization behavior more inline with creator based deserialization.

Full modified test (com.fasterxml.jackson.failing.BuilderDeserializationTest921):

package com.fasterxml.jackson.failing;

import java.util.List;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

public class BuilderDeserializationTest921
    extends BaseMapTest
{
    public static class MyPOJO<X> {
      public X x;

      @JsonCreator
      public MyPOJO(@JsonProperty("x") X x) {
        this.x = x;
      }
    }

  public static class MyPOJOSimple {
    public String x;

    @JsonCreator
    public MyPOJOSimple(@JsonProperty("x") String x) {
      this.x = x;
    }
  }

    @JsonDeserialize(builder = MyGenericPOJO.Builder.class)
    public static class MyGenericPOJO<T, U> {
      private List<T> dataT;
      private List<U> dataU;

      private MyGenericPOJO(List<T> dt, List<U> du) {
        dataT = dt;
        dataU = du;
      }

      public List<T> getDataT() {
        return dataT;
      }

      public List<U> getDataU() {
        return dataU;
      }

      public static class Builder<T, U> {
        private List<T> dataT;
        private List<U> dataU;

        public Builder<T, U> withDataT(List<T> d) {
          dataT = d;
          return this;
        }

        public Builder<T, U> withDataU(List<U> d) {
          dataU = d;
          return this;
        }

        public MyGenericPOJO<T, U> build() {
          return new MyGenericPOJO<>(dataT, dataU);
        }
      }
    }

    public static class MyGenericPOJOWithCreator<T, U> {
      private List<T> dataT;
      private List<U> dataU;

      private MyGenericPOJOWithCreator(List<T> dt, List<U> du) {
        dataT = dt;
        dataU = du;
      }

      @JsonCreator
      public static <X, Y> MyGenericPOJOWithCreator<X, Y> create(
              @JsonProperty("dataT") List<X> dataT,
              @JsonProperty("dataU") List<Y> dataU) {
          return new MyGenericPOJOWithCreator.Builder<X, Y>().withDataT(dataT).withDataU(dataU).build();
      }

      public List<T> getDataT() {
        return dataT;
      }

      public List<U> getDataU() {
        return dataU;
      }

      public static class Builder<T, U> {
        private List<T> dataT;
        private List<U> dataU;

        public Builder<T, U> withDataT(List<T> d) {
          dataT = d;
          return this;
        }

        public Builder<T, U> withDataU(List<U> d) {
          dataU = d;
          return this;
        }

        public MyGenericPOJOWithCreator<T, U> build() {
          return new MyGenericPOJOWithCreator<>(dataT, dataU);
        }
      }
    }

    public void testWithBuilder() throws Exception {
      final ObjectMapper mapper = new ObjectMapper();
      final String json = aposToQuotes("{ 'dataT': [ {'x':'Foo'}, {'x':'Bar'}], 'dataU': [{'x':1}, {'x':2}] }");
      final MyGenericPOJO<MyPOJO<String>, MyPOJO<Integer>> deserialized =
          mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO<String>, MyPOJO<Integer>>>() {});
      assertEquals(2, deserialized.dataT.size());

      Object obT = deserialized.dataT.get(0);
      assertNotNull(obT);
      assertEquals(MyPOJO.class, obT.getClass());
      assertEquals(String.class, ((MyPOJO) obT).x.getClass());

      Object obU = deserialized.dataU.get(0);
      assertNotNull(obU);
      assertEquals(MyPOJO.class, obU.getClass());
      assertEquals(Integer.class, ((MyPOJO) obU).x.getClass());
    }

  public void testWithJsonCreatorSimple() throws Exception {
    final ObjectMapper mapper = new ObjectMapper();
    final String json = aposToQuotes("{ 'dataT': [ {'x':'Foo'}, {'x':'Bar'}], 'dataU': [{'x':1}, {'x':2}] }");
    final MyGenericPOJOWithCreator<MyPOJOSimple, MyPOJOSimple> deserialized =
            mapper.readValue(json, new TypeReference<MyGenericPOJOWithCreator<MyPOJOSimple, MyPOJOSimple>>() {});
    assertEquals(2, deserialized.dataT.size());

    Object obT = deserialized.dataT.get(0);
    assertNotNull(obT);
    assertEquals(MyPOJOSimple.class, obT.getClass());
    assertEquals(String.class, ((MyPOJOSimple) obT).x.getClass());

    Object obU = deserialized.dataU.get(0);
    assertNotNull(obU);
    assertEquals(MyPOJOSimple.class, obU.getClass());
    assertEquals(String.class, ((MyPOJOSimple) obU).x.getClass());
  }

    public void testWithJsonCreatorParameterized() throws Exception {
      final ObjectMapper mapper = new ObjectMapper();
      final String json = aposToQuotes("{ 'dataT': [ {'x':'Foo'}, {'x':'Bar'}], 'dataU': [{'x':1}, {'x':2}] }");
      final MyGenericPOJOWithCreator<MyPOJO<String>, MyPOJO<Integer>> deserialized =
              mapper.readValue(json, new TypeReference<MyGenericPOJOWithCreator<MyPOJO<String>, MyPOJO<Integer>>>() {});
      assertEquals(2, deserialized.dataT.size());

      Object obT = deserialized.dataT.get(0);
      assertNotNull(obT);
      assertEquals(MyPOJO.class, obT.getClass());
      assertEquals(String.class, ((MyPOJO) obT).x.getClass());

      Object obU = deserialized.dataU.get(0);
      assertNotNull(obU);
      assertEquals(MyPOJO.class, obU.getClass());
      assertEquals(Integer.class, ((MyPOJO) obU).x.getClass());
    }
  }

^^ Executed against tag 2.9

@cowtowncoder
Copy link
Member

@vjkoskela Lots to digest, so I'll try to answer just one question:

 I have not found where in the data bind code this path infers the generic parameters from the type reference. 

Type here has to be resolved via full inspection of the enclosing bean/builder type: methods like introspectForCreation() (in DeserializationConfig) have to resolve types for all fields and methods; using declaring class as the context, and then resolving return and parameter types for accessors.
They will then be either fully resolved, or at least resolvable, for accessor objects like AnnotatedMethod.

I don't know how much this helps, but one thing to verify is that resolved type is correct (although type resolution code is in much better shape now, since 2.7, it's pretty complicated thing and bugs are still possible). But the other part then is whether full type information is (and may be) passed across builder, actual type. This is less clear to me.

@ghost
Copy link

ghost commented Jan 25, 2018

Joining the "me too" for this issue, my implementation is as follows:

public <ReturnClass> ReturnClass performRequest(String httpUrl)
{
    String json = performHttpRequest(httpUrl);
    ObjectMapper objectMapper = new ObjectMapper();
    TypeReference<ReturnClass> typeReference = new TypeReference<ReturnClass>(){};
    ReturnClass returnObject = objectMapper.readerFor(typeReference).readValue(json);
    return returnObject; // throws ClassCastException: java.util.LinkedHashMap cannot be cast to MyReturnClassImpl
}

Is there a known solution to this, or should I pass the desired class as a method argument?

@cowtowncoder
Copy link
Member

@hubbazoot Is that using Builder? Without type definitions that isn't of much help unfortunately.
But I guess you are just confirming that with your types you get same exception, so that this is not uncommon problem?

@ghost
Copy link

ghost commented Jan 25, 2018

@cowtowncoder I am not using Builder. I have a POJO that I am expecting as a result and am expecting back. I greatly simplified the code involved to illustrate the core issue. This was the closest ticket I could find that seemed relevant, and this problem is reproducible on my system. If this has been solved elsewhere or could be resolved by overriding a dependency, that would be fantastic.

@JsonIgnoreProperties(ignoreUnknown = true)
class MyPojo
{
    Integer someInteger;
    List<MyOtherPojo> items;
}
//elsewhere in my code
public Object performServiceCall()
{
    //this method defined in my previous comment
    MyPojo myPojo = requestService.performRequest("my http URL"); //throws classCastException
}

I have found a workaround that appears to work, but does slightly undermine the purpose of the template

public <ReturnClass> ReturnClass performRequest(String httpUrl, Class responseClass )
{
    String json = performHttpRequest(httpUrl);
    ObjectMapper objectMapper = new ObjectMapper();
    ReturnClass returnObject = objectMapper.readerFor(responseClass).readValue(json);
    return returnObject; // throws ClassCastException: java.util.LinkedHashMap cannot be cast to MyReturnClassImpl
}

This is in jackson-databind 2.8.10 from spring-boot-starter-web 1.5.9.RELEASE.

@cowtowncoder
Copy link
Member

@hubbazoot Note the title: "Deserialization Not Working Right with Generic Types and Builders" -- this issue is about Builders, so your issue is not same, and likely not related. LinkedHashMap is the result of JSON Object being bound to java.lang.Object (or, basically, ?) typed property or value, and just seeing that tells very little about problem you are having.

If you can reproduce the issue with straight Jackson jackson-databind, please file a new issue.
(and by straight Jackson I mean that it does not use REST frameworks, since those have challenges of their own, issues for which need to be reported against framework).

@markusa380
Copy link

markusa380 commented Feb 27, 2019

I ran into this issue with Spring's RestTemplate.
I use Lombok and the @JsonPOJOBuilder - annotation to trick Jackson into deserializing my
immutable object, but once a type parameter enters the play things get hairy.
I would not mind using the snapshot of Jackson that includes the new MapperFeature.INFER_BUILDER_TYPE_BINDINGS, but Spring does - so I'm stuck here.
What's the latest state on a workaround that doesn't involve an additional creator method?

@cowtowncoder cowtowncoder added 2.11 and removed 2.9 labels Sep 12, 2019
@cowtowncoder
Copy link
Member

[removed a comment after realizing I may have misread the test]

@cowtowncoder cowtowncoder removed the 2.11 label Apr 12, 2020
@vjkoskela
Copy link
Contributor

It's been a while since I've had a chance to write some Java. Well, I got back into it, and wasn't long before I was using Jackson. That of course reminded that I may owe you @cowtowncoder a few things.

It looks like this was 2.9 and then 2.11. Is there anything on this issue I can do to move it forward and get it included in an upcoming release?

@cowtowncoder
Copy link
Member

Looking at #1796, I realize I did merge the patch in master -- but that would only go in 3.0.
I would be open to backporting this into 2.12 (2.11.0 was just released). I'd first need to create 2.12 branch (not a big deal), and then see if cherry-picking might work for change.
I'll add this on my TODO list.

@cowtowncoder cowtowncoder added this to the 2.12.0 milestone Apr 29, 2020
cowtowncoder added a commit that referenced this issue Sep 6, 2020
@cowtowncoder
Copy link
Member

Quick note: after unrelated change (fix for #2821), one test here fails (testWithCreator...()).
That test is not valid: static methods do not refer to type variables of enclosing class.
Commented out that test.

@Samathingamajig
Copy link

is this fixed in the latest version?

@cowtowncoder
Copy link
Member

As per milestone indicated on the right side, was fixed for version 2.12.0 so yes, 2.12.x and 2.13.x should have the fix.

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

9 participants