FasterXML / FasterXML/jackson-databind

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

Open
#3,681 10 comments 0 reactions 0 assignees View on GitHub
has-failing-test will-not-fix
Dominant language
Java
Stars
3.7k
Forks
1.5k
Avg merge
3d 5h
Merged PRs (30d)
30

Description

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...

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the supplied JsonTypeIdTest reproduction and the ObjectMapperProvider.createBuilder().build() entry point, then trace subtype resolution for C and its inherited interfaces. Add a regression test covering both interface declaration orders, and consider the issue done when the behavior is deterministic or reports the conflicting @JsonSubTypes mappings clearly.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.