javaspringspring-bootgraalvm

GraalVM Native Image: Why Spring Native plugin does not add @JsonSubTypes implementations to reflect-config?


I’m using Spring Boot 3.5.3 with GraalVM Native Image via 'org.graalvm.buildtools.native' version '0.11.1' plugin.

I also use Jackson for polymorphic deserialization using @JsonSubTypes.

Example setup:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "Cat")
})
public interface Animal {
    String getName();
}

@Getter
@Setter
public class Dog implements Animal {
    private String name;
}

@Getter
@Setter
public class Cat implements Animal {
    private String name;
}

@Getter
@Setter
public class Zoo {
    private Animal animal;
}

And a simple Spring REST controller:

@RestController
public class ZooController {
    @PostMapping("/zoo")
    public void createZoo(@RequestBody Zoo zoo) {
        System.out.println(zoo.getAnimal().getName());
    }
}

When I build a native image via gradle clean nativeCompile, I see that subtypes Dog and Cat are not included in the build/generated/aotResources/META-INF/native-image/project-path/reflect-config.json, whereas Zoo and Animal are included.

And If I try to call the /zoo endpoint with this request body:

{
  "animal": {
    "@type": "Dog",
    "name": "dog_name"
  }
}

the following exception naturally occurs:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.example.model.Dog: cannot deserialize from Object value (no delegate- or property-based Creator): this appears to be a native image, in which case you may need to configure reflection for the class that is to be deserialized

Question:

Is it possible to set up the org.graalvm.buildtools.native plugin to automatically add @JsonSubTypes implementations to reflect-config.json?

Or should I really add manually each subtype to reflection-config.json (or via RuntimeHintsRegistrar)?


Solution

  • The part that is responsible for exporting the reflection configuration for GraalVM is the BindingReflectionHintsRegistrar.

    If you look at the implementation there is a section that iterates the annotations from Jackson.

    private void registerHintsForClassAttributes(ReflectionHints hints, MergedAnnotation<Annotation> annotation) {
      annotation.getRoot().asMap().forEach((attributeName, value) -> {
      if (value instanceof Class<?> classValue && value != Void.class) {
        if (attributeName.equals("builder")) {
          hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                            MemberCategory.INVOKE_DECLARED_METHODS);
        }
        else {
          hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
        }
      }});
    }
    

    It will by default export the information of the value attribute and there is special handling of the builder attribute for the @JsonDeserialize annotation. However as @JsonSubTypes has its value attribute reference another annotation it doesn't loop back into the @JsonSubTypes.Type annotation and hence appears not to include the specific types (or binding).

    To fix you could add your own RuntieHintsRegistrar for these cases but I would also register an issue with the Spring Framework to fix this, what I think is an, omission.