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 ClassUtil
s 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!