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.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Biju Kunjummen
Biju Kunjummen

Written by Biju Kunjummen

Sharing knowledge about Java, Cloud and general software engineering practices

No responses yet

Write a response