nkatsar
nkatsar

Reputation: 1660

Maintain sub type information while serializing java objects using Jackson, without using wrapper class

I am trying to convert between a JSON file and an abstract class with two subclasses in Java using Jackson. Ideally, I would like to use a JSON as the following:

Json document without wrapper

[ {
  "type" : "lion",
  "name" : "Simba",
  "endangered" : true,
  "action" : "running"
}, {
  "type" : "elephant",
  "name" : "Dumbo",
  "endangered" : false,
  "table" : [ 1.0, 2.0, 3.0 ]
} ]

I have annotated the Animal abstract class as shown on http://www.studytrails.com/java/json/java-jackson-Serialization-polymorphism.jsp

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({ @Type(value = Lion.class, name = "lion"),
    @Type(value = Elephant.class, name = "elephant") })
public abstract class Animal {
    String name;
    boolean endangered;

//Skipped constructor/getters/setters

}

I can successfully read/write it using a wrapper object, but the produced JSON file will include an additional animals object.

JSON document with wrapper

{
  "animals" : [ {
    "type" : "lion",
    "name" : "Simba",
    "endangered" : true,
    "action" : "running"
  }, {
    "type" : "elephant",
    "name" : "Dumbo",
    "endangered" : false,
    "table" : [ 1.0, 2.0, 3.0 ]
  } ]
}

When I use a plain Java list, I can successfully read from the Json document without wrapper but if I try to write it, the output file will be as follows:

output.json

[ {
  "name" : "Simba",
  "endangered" : true,
  "action" : "running"
}, {
  "name" : "Dumbo",
  "endangered" : false,
  "table" : [ 1.0, 2.0, 3.0 ]
} ]

Java code

// Test reading using raw list
JavaType listType = mapper.getTypeFactory()
        .constructCollectionType(List.class, Animal.class);
List<Animal> jsonList = mapper.readValue(new FileInputStream(
        "demo.json"), listType);
jsonDocument = new File(outputFile);
// Test writing using raw list
mapper.writerWithDefaultPrettyPrinter().writeValue(jsonDocument,
        jsonList);

Any ideas how I can include the type information when serializing the objects into JSON?

The complete Eclipse project can be found on: https://github.com/nkatsar/json-subclass

EDIT : Simplified the Java code to use JavaType instead of TypeReference. The code on GitHub has been also updated with the correct solutions

EDIT 2 : As mentioned in the comments, I ended up using Object arrays for serialization/deserialization as follows:

// Test reading using array
Animal[] jsonArray = mapper.readValue(
        new FileInputStream(demoFile), Animal[].class);
System.out.println("Reading using array:\nObject: "
        + Arrays.toString(jsonArray));

// Test writing using array
outputJson = mapper.writeValueAsString(jsonArray);
System.out.println("Writing using array:\nJSON: " + outputJson);

Upvotes: 6

Views: 8009

Answers (1)

Ilya Ovesnov
Ilya Ovesnov

Reputation: 4247

The problem here is Java type erasure, which means that nominal type of List object is List. And since java.lang.Object does not have @JsonTypeInfo, it won't be enabled.

But you can configure Jackson to use specific Java type to convert List elements:

JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, Animal.class);
String outputJson = mapper.writerWithType(listJavaType).writeValueAsString(myList);

Modified your code:

public static void main(String[] args) {

List<Animal> myList = new ArrayList<Animal>();
myList.add(new Lion("Simba"));
// myList.add(new Lion("Nala"));
myList.add(new Elephant("Dumbo"));
// myList.add(new Elephant("Lucy"));
AnimalList wrapperWrite = new AnimalList(myList);

try {
    ObjectMapper mapper = new ObjectMapper();
    JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, Animal.class);

    String outputJson = mapper.writerWithType(listJavaType).writeValueAsString(myList);
    System.out.println(outputJson);

    List wrapperRead = mapper.readValue(outputJson, List.class);
    System.out.println("Read recently generated data: " + wrapperRead);

    // Test reading using raw list
    List<Animal> jsonList = mapper.readValue(new FileInputStream( "demo.json"), listJavaType);
    System.out.println("Read from demo.json \ndata: " + jsonList);

    outputJson = mapper.writerWithType(listJavaType).writeValueAsString(jsonList);
    System.out.println(outputJson);

} catch (JsonGenerationException e) {
    System.out.println("Could not generate JSON: " + e.getMessage());
} catch (JsonMappingException e) {
    System.out.println("Invalid JSON Mapping: " + e.getMessage());
} catch (IOException e) {
    System.out.println("File I/O error: ");
}

}

Result:

[{"type":"lion","name":"Simba","endangered":false,"action":null},{"type":"elephant","name":"Dumbo","endangered":false,"table":null}]
Read recently generated data: [{type=lion, name=Simba, endangered=false, action=null}, {type=elephant, name=Dumbo, endangered=false, table=null}]
Read from demo.json 
data: [Animal(name=Nala, endangered=true, action=running}, Animal(name=Lucy, endangered=false, table=[1.0, 2.0, 3.0]}]
[{"type":"lion","name":"Nala","endangered":true,"action":"running"},{"type":"elephant","name":"Lucy","endangered":false,"table":[1.0,2.0,3.0]}]

Another solution: You can use custom TypeReference to inform Jackson what class needs to be used to convert List:

mapper.writerWithType(new TypeReference<List<Animal>>() {}).writeValueAsString(myList)

That solution will work as well but I like main solution better because it is more clean, you don't need to instantiate abstract 'TypeReference' class...

Upvotes: 5

Related Questions