Make Jackson Subtypes extensible without editing the Supertypes java-file
Asked Answered
S

2

4

In my company we have a fixed JSON message structure:

{
    "headerVal1": ""
    "headerVal2": ""
    "customPayload": {
        "payloadType":""
    }
}

I would like to have some kind of library, which allows me, to not care for the company defined message structure, and instead just send and receive the payload.

My idea was, to define the structure of the company template as one object, and use subtypes of a PayloadObject.

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.MINIMAL_CLASS,
    property = "payloadType",
    visible = false)
public abstract class PayloadObject {
}

Now I can create subclasses of the PayloadObject, and it can be automatically deserialized in this structure, as long as the property payloadType has a string ".SubTypeName".

This is problematic, since I cannot customize it, not even remove the superflous . in the beginning. This is unfortunately not necessarily compatible with other, existing systems in the company, we need to interface with.

The alternative is, to add a @JsonSubTypes-annotation in which I can add all the possible subtypes - which I don't want to know when writing the library. So this option won't work for me.

I thought, it might help to have the @JsonType-annoation with the subtypes, but I still have to add the @JsonSubTypes, which does not help.


Is there a way, to add subtypes to a basetype without modifying the basetypes java-file?


If this helps: We are working with Java Spring.

Shippen answered 29/4, 2019 at 8:43 Comment(4)
Hmm you could try using a mixin to define the subtypes. That way you could provide the subtype definitions from the caller side and with some preprocessing etc. it should even be possible to have those mixins automatically generated.Chronicles
The only thing I learned so far about mix-ins is, how to use them to ignore certain properties, as described here: baeldung.com/jackson-annotations - but I don't know how to use them for - more or less - the opposite.Shippen
Well, you can use mixins to override/add a lot of annotations/configuration. There might be limitations tough - we're using them to some extend but not yet to add/set subtypes.Chronicles
If you are looking for MixIn examples, check: Jackson serialization of abstract interface, Dynamic addition of fasterxml Annotation?, Custom Jackson Serializer for a specific type in a particular class.Haruspex
S
4

ObjectMapper has a method registerSubtypes(NamedType) which can be used to add subtypes for use, without having them in the annotations.

For this I created a new Annotation (I might have reused @JsonTypeName, but it might be abusive)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyJsonSubtype
{
   public String jsonTypeName();
}

Then I wrote me a method

public static void registerMyJsonSubtypes(ObjectMapper om, Object... reflectionArgs) {
    Reflections reflections = new Reflections(reflectionArgs);
    Set<Class<?>> types = reflections.getTypesAnnotatedWith(MyJsonSubtype.class);
    for (Class type : types) {
        String name = ((MyJsonSubtype) type.getAnnotation(MyJsonSubtype.class)).jsonTypeName();
        om.registerSubtypes(new NamedType(type, name));
    }
}

which uses Reflections to get all annotated types declared inside searched packages and registers them as subtypes for the ObjectMapper.

This still requires the @JsonTypeInfo-annotation on the base class to mark the object as potentially extensible, so the mapper knows, which property to use, to resolve the name, but I figure, this is is providable. My main attention was on the problem, that I don't want to declare all future subtypes in an annotation on the base class.

I am a Java beginner though, so please share your thoughts, if this is unnecessary or could/should/must be improved.

Shippen answered 7/5, 2019 at 10:33 Comment(0)
S
0

Adapting derM's accepted answer, you can use classes built in to Spring to avoid the Reflections dependency.

First, make sure the type you intend to subtype is annotated with @JsonTypeInfo. Here I'm using an existing visible property on an immutable object to hold the type info.

@JsonTypeInfo(
    use      = JsonTypeInfo.Id.NAME,
    include  = JsonTypeInfo.As.EXISTING_PROPERTY,
    property = "type",
    visible  = true
)
public class MyJsonType {
    // members ///////////////////////////////////////////////////////
    private final String type;

    // constructor(s) ////////////////////////////////////////////////
    @JsonCreator
    public MyJsonType(@JsonProperty("type") final String type) {
        this.type = type;
    }

    // getters and setters ///////////////////////////////////////////
    public String getType() { return type; } 
}

Second, create your own annotation to mark the types you want to register at runtime, which should hold the name to register the type with.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyJsonSubtypeName {
   public String value();
}

Third, create a configuration to detect classes on the classpath that are annotated with this annotation and register them with Jackson.

@Configuration
public class MyJsonTypeConfiguration {
    // members /////////////////////////////////////////////////////////////////////
    @Autowired
    private final ObjectMapper objectMapper;

    // methods /////////////////////////////////////////////////////////////////////
    @PostConstruct
    public void autoRegisterMyJsonSubtypes() {
        // Build a scanner to find all classes on the class path that are annotated 
        // with @MyJsonSubtypeName.
        final ClassPathScanningCandidateComponentProvider scanner = (
            new ClassPathScanningCandidateComponentProvider(false)
        );
        scanner.addIncludeFilter(new AnnotationTypeFilter(MyJsonSubtypeName.class));

        // Run the scanner to find all classes on the class path within the base 
        // package that are annotated with @MyJsonSubtypeName and register them as 
        // subtypes of the MyJsonType class with Jackson.
        for (final BeanDefinition candidateComponent : scanner.findCandidateComponents("my.base.package")) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // Get the subtype name from the value of the annotation.
                final AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
                final String                  subtypeName    = (String)(
                    beanDefinition
                        .getMetadata             ()
                        .getAnnotationAttributes (MyJsonSubtypeName.class.getCanonicalName())
                        .get                     ("value")
                );

                // Register the class as a subtype of the MyJsonType class with its
                // configured name.
                try {
                    objectMapper.registerSubtypes(new NamedType(
                        /* c    = */ Class.forName(beanDefinition.getBeanClassName()),
                        /* name = */ subtypeName
                    ));
                } catch (final Exception e) {
                    throw new Error(String.format(
                        "Failed to register class %s as a subtype of %s due to " +
                        "an unexpected error.",
                        beanDefinition.getBeanClassName(),
                        MyJsonType.class.getName()
                    ), e);
                }
            }
        }
    }
}

Fourth, and lastly, add the annotation to all classes you want to have registered as subtypes at runtime.

@MyJsonSubtypeName("foo")
public class Foo extends MyJsonType {
    // members ///////////////////////////////////////////////////////
    private final String bar;

    // constructor(s) ////////////////////////////////////////////////
    @JsonCreator
    public Foo(
        @JsonProperty("type") final String type,
        @JsonProperty("bar")  final String bar
    ) {
        super(type);
        this.bar = bar;
    }

    // getters and setters ///////////////////////////////////////////
    public String getBar() { return bar; } 
}
Shove answered 27/3 at 19:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.