javaspringspring-bootswaggeropenapi

SpringBoot OpenAPI definition polymorphism with anyOf and discriminatorProperty


How to define entity inheritance in a SpringBoot openapi type definition when POST-ing a @RequestBody containing an object map of polymorphic entities?

Shouldn't it be sufficient to set anyOf = { PolarParrot.class, NorwegianParrot.class}, discriminatorProperty = "parrotType" in order to type hint which concrete Parrot instance must be used to resolve the RequestBody?

import java.lang.Override;
import java.net.URI;
import java.util.Map;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;

@RestController
public class PolyParrot {
    @PostMapping(value = {"/create"}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> create(@Valid @RequestBody Flock flock) {
        return ResponseEntity.created(URI.create("/flock/42")).build();
    }
}

@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE)
class Flock {
    @Schema
    Map<String, Parrot> birds;

    public Map<String, Parrot> getBirds() {
        return birds;
    }

    public void setBirds(Map<String, Parrot> birds) {
        this.birds = birds;
    }
}

@Schema(anyOf = {
        PolarParrot.class,
        NorwegianParrot.class,
}, discriminatorProperty = "parrotType")
interface Parrot {
    String getParrotType();
    String getPlumage();
}

@Schema()
class PolarParrot implements Parrot {
    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, defaultValue = "polar", example = "polar")
    String parrotType;

    @Override
    public String getParrotType() {
        return parrotType;
    }

    @Override
    public String getPlumage() {
        return "red";
    }
}

@Schema()
class NorwegianParrot implements Parrot {
    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, defaultValue = "norwegian", example = "norwegian")
    String parrotType;

    @Override
    public String getParrotType() {
        return parrotType;
    }

    @Override
    public String getPlumage() {
        return "blue";
    }
}

According to docs and examples it appears that anyOf + discriminatorProperty should be enough, yet, when POST-ing a valid payload instead of a deserialized flock object it throws:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Cannot construct instance of `Parrot` 
(no Creators, like default constructor, exist): 
abstract types either need to be mapped to concrete types, 
have custom deserializer, or contain additional type information

Doesn't Parrot contain enough 'additional type information' already?

Which essential part did I miss here?


Solution

  • com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of Parrot (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

    The error states that, during deserialization it was unable to resolve the subtype in payload with reference to the abstract type.

    Annotate Parrot interface with this and include the key 'type' in the payload like below depending on the subclass accordingly:

    @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, property = "parrotType")
    @JsonSubTypes({
    @JsonSubTypes.Type(value = PolarParrot.class, name = "polarParrot"),
    @JsonSubTypes.Type(value = NorwegianParrot.class, name = 
    "norwegianParrot")
    })
    

    Sample payload:

    {
      "birds": {
        "parrot1": {
      "type":"polarParrot",
          "parrotType": "polar"
        },
        "parrot2": {
    "type":"norwegianParrot",
          "parrotType": "norwegian"
        }
      }
    }