Custom polymorphic type handling with Jackson

Adding support for polymorphic types in Jackson is easy and well-documented here. 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 Command that I wanted to match one-to-one on a specific “Sub-Command” class without having to configure each of these identifiers in a @JsonSubType configuration. Furthermore each of these sub-commands should live in the .command package beneath the base command class. So here is what I came up with:

@JsonTypeInfo(
    use = JsonTypeInfo.Id.CUSTOM, 
    include = JsonTypeInfo.As.PROPERTY,
    property = "command"
)
@JsonTypeIdResolver(CommandTypeIdResolver.class)
public abstract class Command {
    // 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 CommandTypeIdResolver is implemented:

public class CommandTypeIdResolver implements TypeIdResolver {
    private static final String COMMAND_PACKAGE = Command.class.getPackage().getName() + ".command";
    private JavaType mBaseType;
    @Override
    public void init(JavaType baseType) {
        mBaseType = baseType;
    }

    @Override
    public Id getMechanism() {
        return Id.CUSTOM;
    }

    @Override
    public String idFromValue(Object obj) {
        return idFromValueAndType(obj, obj.getClass());
    }

    @Override
    public String idFromBaseType() {
        return idFromValueAndType(null, mBaseType.getRawClass());
    }

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

    @Override
    public JavaType typeFromId(String type) {
        Class<?> clazz;
        String clazzName = COMMAND_PACKAGE + "." + type;
        try {
            clazz = ClassUtil.findClass(clazzName);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("cannot find class '" + clazzName + "'");
        }
        return TypeFactory.defaultInstance().constructSpecializedType(mBaseType, 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 .command package beneath the package where the Command 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 thats already it!

Thanks to the nice folks at the Jackson User Mailing List for pointing me into the right direction!