jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

JsonSubTypes declared on multiple interfaces of a class results in order-dependent resolution and outcome

Open dweiss opened this issue 2 years ago • 8 comments

This is arguably a corner case and (again, arguably) a bug in our code, but I think it'd be nice if jackson could at least return a better message here. TL;DR; version is: if you have an interface extending from more interfaces, each of which declares its own JsonSubTypes, the deserialization can be different and depends on how javac/runtime orders these interfaces in bytecode.

This is a distilled repro:

public class JsonTypeIdTest {
  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
  @JsonSubTypes({
    @JsonSubTypes.Type(name = "c_impl", value = C_Impl.class)
  })
  private interface A {}

  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
  @JsonSubTypes({
    @JsonSubTypes.Type(name = "b_impl", value = B_impl.class),
  })
  private interface B {}

  // Note the order of declarations on inherited interfaces - it makes
  // the difference in how types are resolved here.
  private interface C extends B, A {}

  private static class C_Impl implements C {}
  private static class B_impl implements B {}

  private static class Clazz {
    @JsonProperty public C c;
  }

  @Test
  public void checkSanity() throws IOException {
    var mapper = ObjectMapperProvider.createBuilder().build();
    mapper.readValue(
        """
        {
          "c": {
            "type": "c_impl"
          }
        }
        """,
        Clazz.class);
  }
}

The 'c' field on Clazz declares an instance of C. This is an interface that extends from A and B, each of which declare their implementing types. If you declare C to extend B, A, the above fails for me with:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'c_impl' as a subtype of `com.carrotsearch.lingo4g.internal.util.jackson.JsonTypeIdTest$C`: known type ids = [b_impl] (for POJO property 'c')
 at [Source: (String)"{
  "c": {
    "type": "c_impl"
  }
}
"; line: 3, column: 13] (through reference chain: com.carrotsearch.lingo4g.internal.util.jackson.JsonTypeIdTest$Clazz["c"])

so the type info is read from interface B only. But if you reorder the declaration to A, B, things will work just fine as "c_impl" type indeed implements interface C (and B).

I think the ideal way to solve the above would be to collect type mapping information from all superclasses/interfaces, then run a validation check whether type-class mappings are unique, if so - fine. I'm sure it'd break existing code here and there though...

dweiss avatar Nov 23 '22 09:11 dweiss