jackson-databind: Deserialization Not Working Right with Generic Types and Builders

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));

  }
}

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 4
  • Comments: 28 (17 by maintainers)

Commits related to this issue

Most upvoted comments

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.

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?

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?

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<Z>) 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 https://github.com/FasterXML/jackson-databind/pull/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

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