Swagger and polymorphic type handling with Jackson

polimorphic types with jackson and swaggerPolymorphic type handling based on JSON property

Building an API that supports inheritance could be quite crucial. Imagine the case where you want to have an endpoint that stores a profile data for a given provider and that provider could be your main website or any 3rd party platform that can provide the data. We might have a base Profile class which in our case would be abstract class with firstName and lastName fields and an abstract method to provide the name of the actual profile provider getProfile().

You can do all of that quite easily with few Jackson annotations, for  polymorphic types, on the base class and on the children that extend the base class. No more words here is an example:

@ApiModel(subTypes = {YahooProfile.class,GmailProfile.class}, discriminator = "provider")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "provider")
    @Type(value = YahooProfile.class, name = YahooProfile.NAME),
    @Type(value = GmailProfile.class, name = GmailProfile.NAME)
abstract public class Profile {
    private String firstName;
    private String lastName;
    @XmlElement(name = "provider")
    @ApiModelProperty(required = true, allowableValues = "yahoo,gmail")
    abstract public String getProvider();

The @JsonTypeInfo allows us to configure how type information is used with JSON serialization and deserialization and what strategy we want to use to resolve the hard implementation of the class based on the JSON data. In our case we want to resolve on a string field inside the JSON data and more specifically the provider field. We have few options here, in use you can set it to class and need to provide either fully qualified class name or short class name I prefer to use a string representation JsonTypeInfo.Id.NAME as that makes it easier to read and store in DB for example.

Second part is the include which specifies that we want to use JSON propety JsonTypeInfo.As.PROPERTY and in the property tag we provide the name of the field we want to take the data from, in our case provider.

That’s all for the parent class we now need two hard implementations that will provide different functionality for the two profiles.

@ApiModel(description = "GmailProfile", parent = Profile.class)
public class GmailProfile extends Profile {
    private final static String NAME = "gmail";

    @XmlElement(name = "provider")
    @ApiModelProperty(required = true)
    public String getProvider() {
        return NAME;

In the GmailProfile we define the name of the class which will be used to map the JSON data to the hard implementation. Here there are no Jackson specific annotations just few for Swagger to provide nice documentation on the test UI. If we have extra properties that need to be set for one or another sub-class Jackson will automatically inspect the setters of the class and set all applicable values on the fly.
This approach can be applied for both Request and Response objects in our API.

Jackson custom type resolver

But what if neither the Class-based nor the property-based @JsonSubType default type ID resolvers are fitting your use case?

Enter custom type ID resolvers! In my case a server returned an identifier for a Profile that I wanted to match one-to-one on a specific “Sub-Profile” class without having to configure each of these identifiers in a @JsonSubType configuration. Furthermore each of these sub-profiles should live in the .profile package beneath the base profile class. So here is what I came up with:

@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, property = "provider")
public abstract class Profile
    // common properties here

The important part beside the additional @JsonTypeIdResolver annotation is the use argument that is set to JsonTypeInfo.Id.CUSTOM. Normally you’d use JsonTypeInfo.Id.CLASS or JsonTypeInfo.Id.NAME. Lets see how the ProfileTypeResolver is implemented:

public class ProfileTypeResolver implements TypeIdResolver
    private static final String PROFILE_PACKAGE = Profile.class.getPackage().getName() + ".profile";
    private JavaType baseType;

    public void init(JavaType baseType)
        this.baseType = baseType;

    public Id getMechanism()
        return Id.CUSTOM;

    public String idFromValue(Object obj)
        return idFromValueAndType(obj, obj.getClass());

    public String idFromBaseType()
        return idFromValueAndType(null, baseType.getRawClass());

    public String idFromValueAndType(Object obj, Class<?> clazz)
        String name = clazz.getName();
        if ( name.startsWith(PROFILE_PACKAGE) ) {
            return name.substring(PROFILE_PACKAGE.length() + 1);
        throw new IllegalStateException("class " + clazz + " is not in the package " + PROFILE_PACKAGE);

    public JavaType typeFromId(DatabindContext context, String id)
        Class<?> clazz;
        String clazzName = PROFILE_PACKAGE + "." + type;
        try {
            clazz = ClassUtil.findClass(clazzName);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("cannot find class '" + clazzName + "'");
        return context.constructSpecializedType(baseType, clazz);

The two most important methods here are idFromValueAndType and typeFromId. For the first I get the class name of the class to serialize and check whether it is in the right package (the .profile package beneath the package where the Profile class resides). If this is the case, I strip-off the package path and return that to the serializer. For the latter method I go the other way around: I try to load the class with Jackson’s ClassUtils by using the class name I got from the deserializer and prepend the expected package name in front of it. And that’s already it!


Just a guy with strong interest in PHP and Web technologies

Tagged with: , , , , , ,
One comment on “Swagger and polymorphic type handling with Jackson
  1. robocide says:

    great article ! why not share code ? 🙂

Leave a Reply to robocide Cancel reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.