Reputation: 1660
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
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