Jackson and handling class hierarchy
I am in awe of the community which maintains excellent open source projects like Spring Framework and Jackson. Even after working with them for years I still find interesting and new nuggets of information. One such nugget is how Jackson handles json serialization/deserialization of class hierarchies.
Inheritance and serialization
Consider a simple class hierarchy:
represented the following way in code:
public abstract class Vehicle {
private String make;
private String model;
private String color;
...
}
public class Car extends Vehicle{
private int seats;
...
}
public class Truck extends Vehicle {
private int payloadCapacity;
...
}
Now, given say a “Car” instance:
Car car = new Car();
car.setMake("Toyota");
car.setModel("Camry");
car.setColor("White");
car.setSeats(5);
This can be serialized the using Jackson the following way:
ObjectMapper objectMapper = new ObjectMapper();
String carJson = objectMapper.writeValueAsString(car);
giving this json:
{"make":"Toyota","model":"Camry","color":"White","seats":5}
Now, trying to deserialize it back to a Vehicle:
Vehicle carFromJson = objectMapper.readValue(carJson, Vehicle.class);
would result an exception along these lines:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `org.bk.json.Vehicle`
which is a perfectly reasonable message as there is nothing in the json to indicate that a Car
instance should be created back, so there has to be some kind of hint to Jackson to instantiate a Car
when given this kind of a Json structure and this is done using an annotation in the base class which looks like this:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Car.class, name = "car"),
@JsonSubTypes.Type(value = Truck.class, name = "truck")
})
public abstract class Vehicle {
...
}
This means that Jackson will inject a type
property into the serialized Json with a value of car
or truck
and will use it to deserialize back to a Car instance.
With this in use, a serialized car looks like this:
{"type":"car","make":"Toyota","model":"Camry","color":"White","seats":5}
and the deserialization just works!
One small change that I would normally make is to use an existing property to drive the serialization/deserialization, let’s say a vehicleType property is introduced into the vehicle with hints about the type of Vehicle being serialized, with Car and Truck types self identifying themselves using this information:
public abstract class Vehicle {
...
protected VehicleType vehicleType;
}
public class Car extends Vehicle{
public Car() {
this.vehicleType = VehicleType.CAR;
}
...
}
public class Truck extends Vehicle {
public Truck() {
this.vehicleType = VehicleType.TRUCK;
}
...
}
Now a serialized car would look like this, with the additional vehicleType
attribute:
{"make":"Toyota","model":"Camry","color":"White","vehicleType":"CAR","seats":5}
To make use of this vehicleType
property, Jackson can be provided a hint of the following type:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "vehicleType")
@JsonSubTypes({
@JsonSubTypes.Type(value = Car.class, name = "CAR"),
@JsonSubTypes.Type(value = Truck.class, name = "TRUCK")
})
public abstract class Vehicle {
...
}
and it would deserialize the Car
instance correctly.
Inheritence and Generic Types
This works great with Generic types also, say a few vehicles are created the following way and serialized into a json:
Car car = new Car();
car.setMake("Toyota");
car.setModel("Camry");
car.setColor("White");
car.setSeats(5);
Truck truck = new Truck();
truck.setMake("Ford");
truck.setModel("F150");
truck.setColor("Black");
truck.setPayloadCapacity(5000);
List<Vehicle> vehicles = List.of(car, truck);
String json = objectMapper.writeValueAsString(vehicles);
The resulting Json looks like this:
[
{
"make": "Toyota",
"model": "Camry",
"color": "White",
"vehicleType": "CAR",
"seats": 5
},
{
"make": "Ford",
"model": "F150",
"color": "Black",
"vehicleType": "TRUCK",
"payloadCapacity": 5000
}
]
and now deserializing it back to a List of vehicles the following way, using TypeReference, again just works and the resulting Vehicle in the list would be of the right subtypes of Vehicle:
List<Vehicle> vehicles =
objectMapper.readValue(json, new TypeReference<List<Vehicle>>() {});
Conclusion
It speaks to the power of good tooling in Jackson that inheritance and correctly handling the serialization/deserialization of subtypes is handled cleanly through multiple options by Jackson and I continue to be in awe of the Open Source community and Jackson authors in particular for creating and maintaining such a good project.