smallrye / smallrye-open-api

SmallRye implementation of Eclipse MicroProfile OpenAPI
Apache License 2.0
118 stars 89 forks source link

No polymorphism for responses and non-primitive fields in generated OpenApi spec #1984

Open Lantromipis opened 1 month ago

Lantromipis commented 1 month ago

According to swagger docs polymorphism can be supported using oneOf and discriminator. This way we can achieve polymorphic API responses and polymorphic properties on objects.

However, looks like smallrye open api does not have ability to generate such open api schema. Automatically when mp.openapi.extensions.smallrye.auto-inheritance=BOTH is enabled. It is only possible by manually adding polymorphism to @Schema on fields and @ApiResponse

Suppose we have the following simple class hierarchy. Note: type on BaseClass is used as discriminator.

@Schema
public class BaseClass {
    private String baseField;
    private String type;
    private FieldBaseClass someObjectField;
}

@Schema
public class FieldBaseClass {
    private String baseFieldClassField;
}

@Schema
public class FirstLevelFieldClass extends FieldBaseClass {
    private String firstLevelFieldClassField;
}

@Schema
public class FirstLevelSubclass extends BaseClass{
    private String firstLevelClassField;
}

@Schema
public class SecondLevelSubclass extends FirstLevelSubclass{
    private String secondLevelClassField;
}

Also, there is a simple Jax-Rs endpoint

@Path("/hello")
public class ExampleResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public BaseClass hello() {
        return new BaseClass();
    }
}

The resulting OpenApi spec is as follows (with mp.openapi.extensions.smallrye.auto-inheritance=BOTH)

---
openapi: 3.0.3
info:
  title: demo API
  version: 1.0-SNAPSHOT
paths:
  /hello:
    get:
      tags:
        - Example Resource
      responses:
        "200":
          description: OK
          content:
            text/plain:
              schema:
                $ref: "#/components/schemas/BaseClass"
components:
  schemas:
    BaseClass:
      type: object
      properties:
        baseField:
          type: string
        type:
          type: string
        someObjectField:
          $ref: "#/components/schemas/FieldBaseClass"
    FieldBaseClass:
      type: object
      properties:
        baseFieldClassField:
          type: string
    FirstLevelFieldClass:
      allOf:
        - $ref: "#/components/schemas/FieldBaseClass"
        - type: object
          properties:
            firstLevelFieldClassField:
              type: string
    FirstLevelSubclass:
      allOf:
        - $ref: "#/components/schemas/BaseClass"
        - type: object
          properties:
            firstLevelClassField:
              type: string
    SecondLevelSubclass:
      allOf:
        - $ref: "#/components/schemas/FirstLevelSubclass"
        - type: object
          properties:
            secondLevelClassField:
              type: string

Expected result:

  1. Response for /hello must contain polymorphic oneOf schema with discriminator and mapping
  2. someObjectField of class BaseClass has a polymorphic oneOf schema with discriminator and mapping
  3. User of library can specify discriminator property on base class
  4. User of library can specify discriminator mapping for each subclass. Then this mapping will be used to generate syncthetic polymorphic schema

For example, smallrye should add syncthetic polymorphic schema to schemas, which can look like this:

---
openapi: 3.0.3
info:
  title: demo API
  version: 1.0-SNAPSHOT
paths:
  /hello:
    get:
      tags:
        - Example Resource
      responses:
        "200":
          description: OK
          content:
            text/plain:
              schema:
                $ref: "#/components/schemas/PolymorphicBaseClass"
components:
  schemas:
    PolymorphicBaseClass:
      type: object
      oneOf:
        - $ref: "#/components/schemas/BaseClass"
        - $ref: "#/components/schemas/FirstLevelSubclass"
        - $ref: "#/components/schemas/SecondLevelSubclass"
      discriminator:
        propertyName: type
        mapping:
          baseType: "#/components/schemas/BaseClass"
          firstType: "#/components/schemas/FirstLevelSubclass"
          secondType: "#/components/schemas/SecondLevelSubclass"
    PolymorphicFieldBaseClass:
      type: object
      oneOf:
        - $ref: "#/components/schemas/FieldBaseClass"
        - $ref: "#/components/schemas/FirstLevelFieldClass"
      discriminator:
        propertyName: type
        mapping:
          baseFieldType: "#/components/schemas/FieldBaseClass"
          firstFieldType: "#/components/schemas/FirstLevelFieldClass"
    BaseClass:
      type: object
      properties:
        baseField:
          type: string
        type:
          type: string
        someObjectField:
          $ref: "#/components/schemas/PolymorphicFieldBaseClass"
    FieldBaseClass:
      type: object
      properties:
        baseFieldClassField:
          type: string
    FirstLevelFieldClass:
      allOf:
        - $ref: "#/components/schemas/FieldBaseClass"
        - type: object
          properties:
            firstLevelFieldClassField:
              type: string
    FirstLevelSubclass:
      allOf:
        - $ref: "#/components/schemas/BaseClass"
        - type: object
          properties:
            firstLevelClassField:
              type: string
    SecondLevelSubclass:
      allOf:
        - $ref: "#/components/schemas/FirstLevelSubclass"
        - type: object
          properties:
            secondLevelClassField:
              type: string

Such ploymorphic API spec is extremely important for some forontend TS consumers. For example, popylar rtk-query lib for frontend can not generate polymorphic API without explicit polymorphic typs declaration (using oneOf and discriminator). In addition, polymorphic API gives human consumers ability to define class hierarchy and used discriminator types just by looking at spec.

danielbobbert commented 1 day ago

Your JAX-RS endpoint is returning "text/plain". How would the BaseClass be encoded to text/plain? And if the method implementation would in fact return a FirstLevelSubclass: how would that be encoded and how would a client tell those encodings apart (i.e. how would the client know that the returned object is in fact a FirstLevelSubclass).

smallrye already supports exactly what you are looking for with JSON. Just return "application/json" from your endpoint and use Jackson annotations (JsonTypeInfo, JsonSubTypes, etc.) to annotate which subtypes are allowed for a base type and which property within the JSON can be used by the client to tell different subtypes apart. smallrye will use those to generate the appropriate oneOf and discriminator information in the yaml.