Using Jackson Subtypes to Write Better Code

Using Jackson Subtypes to Write Better Code

How to use subtypes and inheritance with Jackson to automatically convert JSON to the right subclass, resulting in better (and less) code.

In this post, we'll look at how Jackson Subtypes can automatically map JSON to the right subclass, without having to write your own converter.
First, we'll give some background into why this is useful, and then we'll go through some practical examples for two different use cases.

Why are Jackson Subtypes Useful?

You'll sometimes find that you receive JSON HTTP responses that look similar but have a few fields which are different. If you have to map these to Java classes then it can be annoying to have to replicate all of the common fields across each of your DTO classes.

Let's look at an example of the problem.

Let's say you get a response from a service that looks like this:

[
  {
    "id": "1",
    "type": "onlineOrder",
    "quantity": 1,
    "order": {
      "name": "Playstation 5",
      "onlineCode": "123456",
      "referralId": "6789",
      "customerId": "5"
    }
  },
  {
    "id": "2",
    "type": "shopOrder",
    "quantity": 3,
    "order": {
      "name": "Xbox One",
      "shopId": "2",
      "paymentType": "card"
    }
  }
]

We can see that the two objects in the list look similar, but both have a different order object format.
We could map these to DTO objects using the following class:

public class GenericOrderDTO {
    private String id;
    private String type;
    private int quantity;
    private Object order;

    // getters and setters omitted
}

This will work, but it won't give us any indication about what is inside the order object, plus we'll have to do some casting.
We could also use Map<String, String> instead to represent the order object, which will give us a map of field name to value.
In either case, we would need to write our own mapper to figure out if the order is a shop type or an online type and map the fields accordingly.

A better way to do this is to use Jackson's @JsonTypeInfo and @JsonSubTypes annotations with inheritance.

How Jackson Subtypes Work

Jackson has a concept of subtypes, which is similar to a subclass.

With inheritance you have your parent class which contains common fields across each of your subclasses, and your subclasses which contain fields unique to them. Subtypes work in a similar way, with the help of some annotations.

In order to use subtypes we need to have some property in the JSON that tells us which subtype to use. In our example above we have the type field, which states whether the order object is an online order or a shop order. Another way is when the type field is inside the subtype itself.

Let's look at both.

Type Field is on the Root Object

Using our example above, we can map the JSON to the following DTOs:

import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class OrderDTO {
    private String id;
    private int quantity;
    private String type;
    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
            property = "type",
            defaultImpl = UnknownTypeOrderDTO.class
    )
    private OrderTypeDTO orderTypeDTO;

    // getters and setters omitted
}
import com.fasterxml.jackson.annotation.JsonSubTypes;

@JsonSubTypes({
        @JsonSubTypes.Type(value = OnlineTypeOrderDTO.class, name = "onlineOrder"),
        @JsonSubTypes.Type(value = ShopTypeOrderDTO.class, name = "shopOrder")
})
public class OrderTypeDTO {
}
public class OnlineTypeOrderDTO extends OrderTypeDTO {
    private String name;
    private String referralId;
    private String customerId;

    // getters and setters omitted
}
public class ShopTypeOrderDTO extends OrderTypeDTO {
    private String name;
    private String shopId;
    private String paymentType;

    // getters and setters omitted
}
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class UnknownTypeOrderDTO extends OrderTypeDTO {

}

Looking at OrderTypeDTO first, we have added an annotation that lists what class should map to what name.

In the OrderDTO class, we include an annotation that tells Jackson to map the orderTypeDTO to the class where the value of the type field matches the name specified in the @JsonSubTypes annotation.
We also state that the type field is an external property. That is, it's not a field on OrderTypeDTO, but rather on OrderDTO.
Finally, we state that if Jackson can't find a matching class based on the value of type, then use UnknownTypeOrderDTO instead. This is protect us from exceptions where the order endpoint might add a new type before we change our code. You can then have some code that appropriately deals with any orders that map to UnknownTypeOrderDTO.

Type field is on the Subtype Object

Another way is when the type field is on the subtype object itself.
Let's change our example slightly such that the JSON we receive looks like this:

[
  {
    "id": "1",
    "quantity": 1,
    "order": {
      "name": "Playstation 5",
      "type": "onlineOrder",
      "onlineCode": "123456",
      "referralId": "6789",
      "customerId": "5"
    }
  },
  {
    "id": "2",
    "quantity": 3,
    "order": {
      "name": "Xbox One",
      "type": "shopOrder",
      "shopId": "2",
      "paymentType": "card"
    }
  }
]

In this case, our DTOs would look like this:

import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class OrderDTO {
    private String id;
    private int quantity;
    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            property = "type",
            defaultImpl = UnknownTypeOrderDTO.class
    )
    private OrderTypeDTO orderTypeDTO;

    // getters and setters omitted
}
import com.fasterxml.jackson.annotation.JsonSubTypes;

@JsonSubTypes({
        @JsonSubTypes.Type(value = OnlineTypeOrderDTO.class, name = "onlineOrder"),
        @JsonSubTypes.Type(value = ShopTypeOrderDTO.class, name = "shopOrder")
})
public class OrderTypeDTO {
    private String type;
}

The ShopTypeOrderDTO, OnlineTypeOrderDTO, and UnknownTypeOrderDTO classes are exactly the same as before.

The difference here is that the type field has moved to the OrderTypeDTO class, and include = JsonTypeInfo.As.EXTERNAL_PROPERTY was removed from the @JsonTypeInfo annotation.

Conclusion

In this post, we've expressed when and why Subtypes are useful, as well as learned how to use them when you have the type inside or outside of the subclass through practical examples.

Till next time!