Skip to content
Polymorphism and Inheritance with Jackson

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.

Elevate your Load Testing!
Request a Demo

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. A Truck 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.

Want to become a super load tester?
Request a Demo