jackson-module-android-record: Class annotations and polymorphic types are ignored when deserializing record fields
Suppose a class hierarchy like this:
public record Record(Field field) {
}
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="@class")
@JsonSubTypes({
@JsonSubTypes.Type(value = StringField.class, name = "string"),
@JsonSubTypes.Type(value = IntField.class, name = "int")
})
public abstract class Field {
}
public class StringField extends Field {
private final String val;
@JsonCreator
public StringField(@JsonProperty("val") String val) {
this.val = val;
}
public String getVal() {
return val;
}
}
public class IntField extends Field {
private final int val;
@JsonCreator
public IntField(@JsonProperty("val") int val) {
this.val = val;
}
public int getVal() {
return val;
}
}
Using Jackson on standard desktop Java SE, the following code works as expected:
String serialized = new ObjectMapper().writeValueAsString(r);
new ObjectMapper().readValue(serialized, Record.class);
However, that is not the case on Android (using the AndroidRecordModule), where the deserializer is oblivious of all class annotations (and, consequently, the JsonTypeInfo), and thus is unable to deserialize the abstract type Field. (abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information)
I would presume that the issue lies within this code: https://github.com/FasterXML/jackson-modules-base/blob/9b2fe00f8f724749e6ba2c93efe1b26d3f33442b/android-record/src/main/java/com/fasterxml/jackson/module/androidrecord/AndroidRecordModule.java#L153since in this case, neither the TypeDeserializer, nor getClassAnnotations are used for Field.
My current workaround is to wrap objects akin to Field in this example into the following:
public class ContainerObject<T> { //Android Studio's opinion of this code is "Class can be converted to record class". Well, if only I could:)
public final T value;
@JsonCreator
public ContainerObject(@JsonProperty("value") T value) {
this.value = value;
}
}
and then change Record to be
public record Record(ContainerObject<Field> field) {
}
Using this wrapper, deserialization works as expected on Android. However, the general issue seems to be a bug within Jackson's AndroidRecordModule.
If so desired, I'm willing to fix this myself and PR it.
Hi @HelloOO7, thanks for reporting this. I would appreciate a PR with a fix, and I'll gladly review it (though I can't approve). If you could, please check if it fixes any tests in the failing package.
On an unrelated note, out of curiosity, any reason why, in your sample class hierarchy, Field shouldn't be an interface and StringField & IntField records?
Would be interesting to have a test for regular Java SDK Records with similar set up (for jackson-databind). I assume that should work but I am not sure this is covered.
any reason why, in your sample class hierarchy, Field shouldn't be an interface and StringField & IntField records
In this specific sample, no. In some of my real use cases, however, I use member inheritance for fields and base methods, which, although mostly substitutable through interface default methods, I find preferable to using records.
Would be interesting to have a test for regular Java SDK Records with similar set up
In this case, the problem was that the ValueInstantiator that was being used simply never resolved the actual polymorphic type, which, in standard Jackson deserialization flow, should most likely not happen.
the deserializer is oblivious of all class annotations
I would also like to correct myself on this statement - it appears that type annotations are preserved properly, only the resolution of the type info was actually flawed.