I am trying to create an java SDK for a front-end library that takes JSON input. Essentially, this SDK converts objects of certain type into JSON which is then consumed by that front-end library.
I am using jackson's polymorphic serialization/deserialization using its annotation system.
I have a base class A and 2 child classes B and C extending A. Class A has a type field, using which I decide what class (B or C) is to be used. The syntax looks something like this:
#JsonTypeInfo({
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property= "type"
})
#JsonSubTypes({
#JsonSubTypes.Type(value = B.class, name = "b"),
#JsonSubTypes.Type(value = C.class, name = "c")
})
public class A {
private String type;
public void setType(String type){
this.type = type;
}
public String getType(){
return this.type;
}
}
public class B extends A {
}
public class C extends A {
}
So now, when I use Jackson's ObjectMapper's readValue function and read the stringified JSON and convert to class A, I get the correct instance of either class A or class B based on the value of the type variable. However, and here is the actual problem, when I try to use the function getType I always get null in those objects. I am not sure why jackson is not setting those values on the object.
String json = "{ type: 'b' }"; // example input json
ObjectMapper om = new ObjectMapper();
A a = om.readValue(json, A.class);
// a is actually an instance of class B
a.getType()// this is null
You need to add to #JsonTypeInfo the parameter visible = true to avoid to remove the type when deserializing.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property= "type",
visible = true
)
public abstract boolean visible() default false
Property that defines whether type identifier value will be passed as part of JSON stream to deserializer (true), or handled and
removed by TypeDeserializer (false). Property has no effect on
serialization. Default value is false, meaning that Jackson handles
and removes the type identifier from JSON content that is passed to
JsonDeserializer.
Related
I have old Pojos inside jar. Example;
public class human implements Serializable {
}
public class Man extend human {
}
public class Woman extend human {
}
I have json like {"type":"man",...}
I want to deserialize proper class using type But I cannot use these annotations because Base class generated automatically from xsd.
#JsonTypeInfo(// use = JsonTypeInfo.Id.NAME, // include =
JsonTypeInfo.As.EXISTING_PROPERTY, // property = "queryType", //
visible = true) #JsonSubTypes({ // #Type(value =
ScenarioByCountryQuery.class, name = "scenarioByCountry"), //
#Type(value = ScenarioByMeasureQuery.class, name =
"scenarioByMeasure") })
I don want to desearlize like if type = '' etc.
I want to deserialize dynamically using this type value.
I'm trying to deserialize this json data into list of objects:
[{
"a": {
"commonField": 1,
"aField": "AAA"
}
}, {
"b": {
"commonField": 2,
"bField": "BBB"
}
}]
Each object may be one of several types having both common and unique fields. Information about exact shape of an object is stored in json as key in the wrapper object.
I created corresponding classes for every known shape (set of unique fields) extending class containing all common fields. Also, I added Jackson annotations to the classes to enable polymorphic deserialization. Simplified, resulting classes look like this:
#JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(KeyBasedSubTypeA.class),
#JsonSubTypes.Type(KeyBasedSubTypeB.class)
})
public abstract class KeyBasedSuperType {
public String type;
public int commonField;
}
#JsonTypeName("a")
public class KeyBasedSubTypeA extends KeyBasedSuperType {
public String aField;
}
#JsonTypeName("b")
public class KeyBasedSubTypeB extends KeyBasedSuperType {
public String bField;
}
With this setup Jackson works almost perfectly. It is able to choose correct subtype during deserialization and populate all the fields including common and unique. However, the type field is not updated by Jackson, the key value used for selecting subtype is not stored anywhere. In other words, the data is deserialized into following structure:
[KeyBasedSubTypeA { type=null; commonField=1; aField=AAA },
KeyBasedSubTypeB { type=null; commonField=2; bField=BBB }]
Note type field having null value. So, the question is - How can I make Jackson to store wrapper's key used for selecting subtype somewhere in resulting object?
Here is my JUnit test for the process
public class PolymorphicTest {
private static ObjectMapper mapper;
#BeforeClass
public static void init() {
mapper = new ObjectMapper();
}
#Test
public void testKeyDenominator() throws IOException {
TypeReference<List<KeyBasedSuperType>> dataShape =
new TypeReference<List<KeyBasedSuperType>>() {};
List<KeyBasedSuperType> result = mapper.readValue(
PolymorphicTest.class.getResourceAsStream("polymorphic-key.json"), dataShape);
assertEquals(2, result.size());
assertEquals(KeyBasedSubTypeA.class, result.get(0).getClass());
assertEquals(KeyBasedSubTypeB.class, result.get(1).getClass());
assertEquals(1, result.get(0).commonField);
assertEquals(2, result.get(1).commonField);
assertEquals("a", result.get(0).type); // <---- this line fails
assertEquals("b", result.get(1).type); // <---- this line fails
assertEquals("AAA", ((KeyBasedSubTypeA) result.get(0)).aField);
assertEquals("BBB", ((KeyBasedSubTypeB) result.get(1)).bField);
}
}
The solution actually was very close, just missed a tiny step forward. It is #JsonTypeInfo(visible=true) required to make Jackson handle type info as normal property.
#JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT, property = "type", visible = true)
#JsonSubTypes({
#JsonSubTypes.Type(KeyBasedSubTypeA.class),
#JsonSubTypes.Type(KeyBasedSubTypeB.class)
})
public abstract class KeyBasedSuperType {
public String type;
public int commonField;
}
What I Want to Do
I want to use Jackson to deserialize a polymorphic type, using the standard #JsonTypeInfo annotation as follows:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = As.EXISTING_PROPERTY,
property = "identifier")
#JsonSubTypes({#Type(value = A.class, name = "A"),
#Type(value = B.class, name = "B")})
abstract Class Base {}
Class A implements Base {
public String identifier = "A";
}
Class B implements Base {
public String identifier = "B";
}
Class Decorated {
public String decoration = "DECORATION";
#JsonUnwrapped
public Base base;
}
/*
Serialized instance of Decorated WITHOUT #JsonUnwrapped:
{
"decoration" : "DECORATION",
"base" : {
"identifier" : "A"
}
}
Serialized instance of Decorated WITH #JsonUnwrapped:
{
"decoration" : "DECORATION",
"identifier" : "A"
}
*/
Related post: Deserialize JSON with Jackson into Polymorphic Types - A Complete Example is giving me a compile error
This can normally be deserialized by Jackson as follows:
public Object deserialize(String body, Class clazz) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(body, clazz);
}
(And this would work if the #JsonUnwrapped annotation were removed)
The Problem
Polymorphic types do not play well with Jackson's #JsonUnwrapped annotation, as discussed in this Jira ticket from 2012:
http://markmail.org/message/pogcetxja6goycws#query:+page:1+mid:pogcetxja6goycws+state:results
Handle polymorphic types with #JsonUnwrapped
Agreed - while fixing things is obviously preferable, improving error messages would be useful if that can't be done.
Unwrapping is one of features where implementations gets complicated enough that any bugs cropping up (on deserialization esp) tend to be antibiotic-resistant...
Hardly encouraging.
Three years later:
http://markmail.org/message/cyeyc2ousjp72lh3
Handle polymorphic types with #JsonUnwrapped
Resolution: Won't Fix
Damn.
So, is there any way to coax Jackson into giving me this behaviour without modifying deserialize() or removing the #JsonUnwrapped annotation?
My SinglePolyUnwrappedDeserializer from this Gist can handle a single polymorphic #JsonUnwrapped property. It's in Kotlin, but can easily be ported to Java if needed. Example:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes(
JsonSubTypes.Type(value = A::class, name = "a"),
JsonSubTypes.Type(value = B::class, name = "b")
)
abstract class Base
data class A(val x: Int) : Base()
data class B(val y: Boolean) : Base()
#JsonDeserialize(using = SinglePolyUnwrappedDeserializer::class)
data class C(val a: String, #JsonUnwrapped val b: Base)
AFAIK, all combinations of other annotations are supported. The only limitation is that there is exactly one #JsonUnwrapped property.
If you also need a generic serializer for polymorphic #JsonUnwrapped, you can write it yourself very easily without any reflection or introspection: just merge the ObjectNode of the inner object onto the ObjectNode of the containing object.
I have this response:
{
"id":"decaa828741611e58bcffeff819cdc9f",
"statement":"question statement",
"exercise_type":"QUESTION"
}
Then, based on exercise_type attribute, I want to instantiate different objects instances (subclasses of ExerciseResponseDTO), so I create this mix in:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type")
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
public abstract class ExerciseResponseDTO {
private String id;
private String statement;
#JsonProperty(value = "exercise_type") private String exerciseType;
// Getters and setters
}
public class ExerciseQuestionResponseDTO
extends ExerciseResponseDTO {}
public class ExerciseChoiceResponseDTO
extends ExerciseResponseDTO {}
So I create my ObjectMapper as follows
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(ExerciseResponseDTO.class, ExerciseMixIn.class);
My test:
ExerciseResponseDTO exercise = mapper.readValue(serviceResponse, ExerciseResponseDTO.class)
Assert.assertTrue(exercise.getClass() == ExerciseQuestionResponseDTO.class); // OK
Assert.assertEquals("decaa828741611e58bcffeff819cdc9f" exercise.getId()); // OK
Assert.assertEquals("question statement", exercise.getStatement()); // OK
Assert.assertEquals("QUESTION", exercise.getExerciseType()); // FAIL. Expected: "QUESTION", actual: null
The problem is that, for some reason, the exercise_type attribute being used as property on #JsonTypeInfo is being mapped as null.
Any idea how i can solve this?
Finally, I've found the solution in the API Doc
Note on visibility of type identifier: by default, deserialization
(use during reading of JSON) of type identifier is completely handled
by Jackson, and is not passed to deserializers. However, if so
desired, it is possible to define property visible = true in which
case property will be passed as-is to deserializers (and set via
setter or field) on deserialization.
So the solution was simply adding the 'visible' attribute as follows
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type",
visible = true)
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
As per #jscherman answer by setting, 'visible' true in JsonTypeInfo will help in accessing exercise_type as a field.
If you use the same class to serialize also then resulting JSON will have exercise_type appear twice. So it's better to also update include to JsonTypeInfo.As.EXISTING_PROPERTY
And it's also worth looking at all other options for include.
I'm familiar with the normal polymorphic deserialization stuff where you deserialize an object based on the string value of a certain field. For instance:
#JsonSubTypes(
{
#JsonSubTypes.Type(value = LionCage.class, name = "LION"),
#JsonSubTypes.Type(value = TigerCage.class, name = "TIGER"),
}
)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
Is there any way to do basically the same thing if the "type" field of the incoming object is an integer instead of a string? So in the example above, "LION" and "TIGER" would be 1 and 2. For whatever reason, I haven't been able to figure this out.
Also, how should I have been able to figure this out? Seems like it should be something obvious.
Jackson automatically converts string to numbers and vice versa. Just use string values for numbers. Like "1" for 1 value. Just try this yourself (jackson version is 2.5.1):
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
public class HelloWorldJacksonNumber {
public static class A extends Base {
String a;
}
public static class B extends Base {
String b;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = A.class, name = "1"),
#JsonSubTypes.Type(value = B.class, name = "2")})
public static class Base {
int type;
}
public static void main (String[] args) throws Exception {
final ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.version());
System.out.println(objectMapper.writeValueAsString(new A()));
System.out.println(objectMapper.writeValueAsString(new B()));
System.out.println(objectMapper.readValue("{\"type\":\"1\"}", Base.class).getClass());
System.out.println(objectMapper.readValue("{\"type\":\"2\"}", Base.class).getClass());
}
}
Output is:
2.5.1
{"type":"1"}
{"type":"2"}
class HelloWorldJacksonNumber$A
class HelloWorldJacksonNumber$B
No, that's not an option via the annotations. The TypeIdResolver interface takes and returns strings. You could do it with a custom parser/serializer using Jackson's stream API, but that seems like a lot of work to switch it to numeric field. I would only do it if someone else's system required it. If I owned the whole thing, I would just use the setup you posted in the question.