Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
The deserialization does not seem to work properly with @JsonTypeInfo(use = Id.MINIMAL_CLASS), when the deserialized class is in a sub-package. According to the javadoc MINIMAL_CLASS uses only the classname AND a part of the package name, if it´s needed to construct a fully qualified name. I think exactly this determination of the package name, isnt working quite right for some specific cases.
Version Information
2.19.1
Reproduction
Considering a base class in package example:
package example;
@JsonTypeInfo(use = Id.MINIMAL_CLASS)
public class Base {}
And a class Derived that extends Base, but is located in a sub package:
package example.sub;
public class Derived extends Base{}
Now the following tests are used to serialize and deserialize both Base and Derived as Type Base:
public class Tests {
private static final System.Logger logger = System.getLogger(Tests.class.getName());
public static final ObjectMapper MAPPER = new ObjectMapper();
@Test void base() throws Exception {
var json = MAPPER.writeValueAsString(new Base());
logger.log(Level.INFO, json);
MAPPER.readValue(json, Base.class);
}
@Test void derived() throws Exception {
var json = MAPPER.writeValueAsString(new Derived());
logger.log(Level.INFO, json);
assertThrows(JsonProcessingException.class, () -> {
MAPPER.readValue(json, Base.class);
});
}
}
Expected behavior
The issue is that while trying to deserialize the Derived_json as Base, a JsonProcessingException is raised. The MINIMAL_CLASS annotation forces the use of .Derived as a name in the json, although for a correct deserialization sub.Derived is needed. So basically the check how much of the full qualified package name has to be included is not working correctly.
Additional context
For more context here´s an example of a container class, in which the serialization works just fine:
public class Container {
public Base item = new Derived();
}
Now the corresponding test:
@Test void container() throws Exception {
var json = MAPPER.writeValueAsString(new Container());
logger.log(Level.INFO, json);
MAPPER.readValue(json, Container.class);
}
When Container is de-/serialized it works just as expected with json being {"item":{"@c":".sub.Derived"}}, in which .sub.Derived is used. The problem seems to be very specific to the name determination of classes that would have to be checked by (recursive) reflection and not to fields like in Container.
Depending on how you see the issue, it could also be part of Jackson-Annotation Issues, but I figuered it would fit better here. Thanks to all :-)
Comment From: cowtowncoder
Problem here is Java Type Erasure, I think. When serializing polymorphic type as root value (direct argument for writeValue()
), determination of "base type" (base type of polymorphic type hierarchy to use) uses value.getClass()
, considering Derived
to be the base type, not Base
(determination cannot be done using class hierarchy for other reasons, specifically flattening of annotations for JacksonAnnotationIntrospector
).
I suspect that if value was not root value but, say, via wrapper:
class Wrapper {
public Base value;
}
type id would be produced as expected. But you may also be able to indicate type to use with:
String json = mapper.writerFor(Base.class).writeValueAsString(subtypeValue);
since in both of above cases true, declared base type of Base.class
is known, for purpose
In general I strongly recommend against every directly using polymorphic types as root values due to problems involved -- they can be avoided when root type is non-polymorphic and all polymorphic values are referenced as properties from there.