Jackson and handling class hierarchy

Biju Kunjummen
3 min readJun 28, 2024

--

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.

--

--

Biju Kunjummen

Sharing knowledge about Java, Cloud and general software engineering practices