Fork me on GitHub

Jackson utilities

SPI based polymorphism

In Jackson, deserialization of polymorphic types can be problematic, especially when implementations of a type can be provided by other libraries. Class name based type identifiers can be used, but instantiation of arbitrary classes based on user input can lead to security issues:

package org.example;

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
interface MyInterface {}

On the other side using name based type identifiers requires known subtypes to be declared in base type:

package org.example;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
    @JsonSubTypes.Type(FirstSubType.class),
    @JsonSubTypes.Type(SecondSubType.class),
    // ...
})
interface MyInterface {}
class FirstSubType implements MyInterface {}
@JsonTypeName("CustomName")
class SecondSubType implements MyInterface {}

The class JacksonSpiTypeIdResolver provides a SPI based mechanism to discover new subtypes in runtime using the annotated type FQN as service:

package org.example;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonTypeIdResolver(JacksonSpiTypeIdResolver.class)
interface MyInterface {}
class FirstSubType implements MyInterface {}
@JsonTypeName("CustomName")
class SecondSubType implements MyInterface {}

Providing META-INF/services/org.example.MyInterface files with implementations FQNs adds subtypes to the base type:

org.example.FirstSubType
org.example.SecondSubType
assertInstanceOf(
    FirstSubType.class,
    mapper.readValue(
        "{ \"@type\" : \"FirstSubType\" }",
        MyInterface.class));
assertInstanceOf(
    SecondSubType.class,
    mapper.readValue(
        "{ \"@type\" : \"CustomName\" }",
        MyInterface.class));

The provider detects annotation inheritance, discarding subtypes that don't extend target type:

package org.example;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonTypeIdResolver(JacksonSpiTypeIdResolver.class)
interface MyInterface {}
interface ExtendedInterface extends MyInterface {}
class FirstSubType implements MyInterface {}
@JsonTypeName("CustomName")
class SecondSubType implements ExtendedInterface {}

assertThrows(
    JsonProcessingException.class,
    mapper.readValue(
        "{ \"@type\" : \"FirstSubType\" }",
        ExtendedInterface.class));
assertInstanceOf(
    SecondSubType.class,
    mapper.readValue(
        "{ \"@type\" : \"CustomName\" }",
        ExtendedInterface.class));