Polymorphism and Inheritance with Jackson
Jackson Json is a powerful Java library to serialize and deserialize objects to/from Json.
You may ask yourself:
- How can I serialize and deserialize polymorphic class instances?
- How to configure Jackson to serialize objects being represented by their interface?
Good news! The answer is just below.
It's somehow difficult to find real examples showing how to do this. That's why I've decided to make this little tutorial to help you get the idea quickly with practical code examples.
Polymorphism¶
Polymorphism is the ability to have different implementations represented by a single interface or abstract class. This article describes how to serialize and deserialize objects by their interface, as well as Polymorphic Tree Structured object instances.
Please note this example is written in Java 8 and uses Lombok. Lombok has numerous benefits like generating getters, toString, equals and hashcode methods.
Dependencies¶
First, let's add the Maven dependencies to our pom.xml
:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.3</version>
</dependency>
It's recommended to always use the latest version. Check Jackson Databind on Maven Central.
Example¶
Let's take the following example:
public interface Vehicle {
String getName();
}
It has a Car
implementation:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import static java.util.Objects.requireNonNull;
@Value
public class Car implements Vehicle {
String name;
@JsonCreator
public Car(@JsonProperty("name") final String name) {
this.name = requireNonNull(name);
}
}
And a Truck
implementation:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import static java.util.Objects.requireNonNull;
@Value
public class Truck implements Vehicle {
String name;
@JsonCreator
public Truck(@JsonProperty("name") final String name) {
this.name = requireNonNull(name);
}
}
Implementations¶
The Vehicle
interface has two implementations: Car
and Truck
. Now let's suppose we want to serialize the following object:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import java.util.List;
import static java.util.Objects.requireNonNull;
@Value
public class Vehicles {
List<Vehicle> vehicles;
@JsonCreator
public Vehicles(@JsonProperty("vehicles") final List<Vehicle> vehicles) {
super();
this.vehicles = requireNonNull(vehicles);
}
}
Configure Jackson Json¶
In order to be able to serialize / deserialize the Vehicles
instance, Jackson Json needs to know how to instantiate a Vehicle
instance. It needs to know whenever it's a Truck
or a Car
instance. In order to do this, Jackson Json must be configured. There are several ways to do so.
Direct mapping¶
In the following example, the mapping is directly configured on the Vehicle
interface:
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME;
@JsonTypeInfo(use = NAME, include = PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value=Truck.class, name = "Truck"),
@JsonSubTypes.Type(value=Car.class, name = "Car")
})
public interface Vehicle {
String getName();
}
This solution works well when the interface and the implementation are placed within the same package. However, it introduces a cyclic dependency between the Vehicle
interface and its implementations.
Type mapping¶
The other solution is the configure the type mapping separately on the ObjectMapper
. Let's see how in this JUnit test:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
public class VehicleTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.registerSubtypes(new NamedType(Truck.class, "Truck"));
MAPPER.registerSubtypes(new NamedType(Car.class, "Car"));
}
@Test
public void shouldSerializeVehicles() throws IOException {
final Vehicles vehicles = new Vehicles(ImmutableList.of(new Car("Dodge"), new Truck("Scania")));
final String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(vehicles);
final Vehicles read = MAPPER.readValue(json, Vehicles.class);
assertEquals(vehicles, read);
}
}
This JUnit does the following:
- It configures the type mapping in a
static
block. ATruck
instance is mapped to the named type Truck for example, - And It runs a JUnit which checks the serialization / deserialization produces exactly the same object.
If we take a look at the produced Json, it looks like the following:
{
"vehicles" : [ {
"@type" : "Car",
"name" : "Dodge"
}, {
"@type" : "Truck",
"name" : "Scania"
} ]
}
Jackson has added a @type
attribute to each vehicle json. This special attribute is used to identify the type of vehicle being serialized. Jackson then uses this information during deserialization to create the right class instance.
Polymorphic tree structure¶
Serialization¶
Great! Now that we've covered a basic example, let's see how to serialize a polymorphic tree structure. Here is an example:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import java.util.List;
@Value
public class CarTransporter implements Vehicle {
String name;
List<Vehicle> vehicles;
@JsonCreator
public CarTransporter(
@JsonProperty("name") final String name,
@JsonProperty("vehicles") final List<Vehicle> vehicles) {
super();
this.name = requireNonNull(name);
this.vehicles = requireNonNull(vehicles);
}
}
A CarTransporter
is a Vehicle
itself! But, it's also able to carry Vehicle
instances. In fact, it could technically carry another CarTransporter
although a real one wouldn't be able too... It's not the point here.
Unit test¶
Now let's include another unit-test to try this out:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
public class VehicleTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.registerSubtypes(new NamedType(Truck.class, "Truck"));
MAPPER.registerSubtypes(new NamedType(Car.class, "Car"));
MAPPER.registerSubtypes(new NamedType(CarTransporter.class, "CarTransporter"));
}
@Test
public void shouldSerializeCarTransporter() throws IOException {
final Vehicle transporter = new CarTransporter("Transporter", ImmutableList.of(new Car("Dodge"), new Truck("Scania")));
final String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transporter);
final Vehicle read = MAPPER.readValue(json, Vehicle.class);
assertEquals(transporter, read);
}
}
Json¶
As you can see, I've added the CarTransporter
type mapping. The unit-test above serializes and deserializes a CarTransporter
instance. The produced json looks like the following:
{
"@type" : "CarTransporter",
"name" : "Transporter",
"vehicles" : [ {
"@type" : "Car",
"name" : "Dodge"
}, {
"@type" : "Truck",
"name" : "Scania"
} ]
}
I hope you see now that there is nothing difficult here! Jackson can serialize and deserialize polymorphic data structures very easily. The CarTransporter
can itself carry another CarTransporter
as a vehicle: that's where the tree structure is!
Now, you know how to configure Jackson to serialize and deserialize objects being represented by their interface.