Issues generating an OpenAPI spec using Micronaut-openapi for sealed Kotlin classes

637 views Asked by At

I'm having trouble with sealed classes. I get a specification from Micronaut-openapi, but the code generator I am using (orval) experiences a cyclic reference and fails.

Given this data class:

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type",
    visible = true
)
@JsonSubTypes(
    JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
    JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
    abstract val id: Int

    @JsonTypeName("lounge")
    data class Lounge(
        override val id: Int,
        val hasTv: Boolean,
    ) : AnonymousResponse()

    @JsonTypeName("diningRoom")
    data class DiningRoom(
        override val id: Int,
        val hasTable: Boolean,
    ) : AnonymousResponse()
}

Micronaut-openapi generates the following components:

components:
  schemas:
    AnonymousResponse:
      type: object
      properties:
        id:
          type: integer
          format: int32
      discriminator:
        propertyName: type
        mapping:
          lounge: '#/components/schemas/AnonymousResponse.Lounge'
          diningRoom: '#/components/schemas/AnonymousResponse.DiningRoom'
      oneOf:
      - $ref: '#/components/schemas/AnonymousResponse.Lounge'
      - $ref: '#/components/schemas/AnonymousResponse.DiningRoom'
    AnonymousResponse.DiningRoom:
      allOf:
      - $ref: '#/components/schemas/AnonymousResponse'
      - required:
        - hasTable
        - id
        type: object
        properties:
          id:
            type: integer
            format: int32
          hasTable:
            type: boolean
    AnonymousResponse.Lounge:
      allOf:
      - $ref: '#/components/schemas/AnonymousResponse'
      - required:
        - hasTv
        - id
        type: object
        properties:
          id:
            type: integer
            format: int32
          hasTv:
            type: boolean

Which leads to the following error in orval:

src/models/anonymousResponseDiningRoom.ts:10:13 - error TS2456: Type alias 'AnonymousResponseDiningRoom' circularly references itself.

10 export type AnonymousResponseDiningRoom = AnonymousResponse & AnonymousResponseDiningRoomAllOf;
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/models/anonymousResponse.ts:11:13 - error TS2456: Type alias 'AnonymousResponse' circularly references itself.

11 export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;
               ~~~~~~~~~~~~~~~~~

src/models/anonymousResponseLounge.ts:10:13 - error TS2456: Type alias 'AnonymousResponseLounge' circularly references itself.

10 export type AnonymousResponseLounge = AnonymousResponse & AnonymousResponseLoungeAllOf;
               ~~~~~~~~~~~~~~~~~~~~~~~

I am not entirely sure whether it's the specification generator or the code generator doing something wrong, but the "allOf" - AnonymousResponse references looks iffy to me, as (at least from how I read it) it would lead to e.g. Lounge also containing information from DiningRoom?

1

There are 1 answers

0
DanneJ On

After trying placing a @Schema annotation on the sealed class, and looking into how to customise the automatic Schema generation, I realised this worked:

// No Schema here
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type",
    visible = true
)
@JsonSubTypes(
    JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
    JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
    abstract val id: Int

    @Schema // Schema here
    data class Lounge(
        override val id: Int,
        val hasTv: Boolean,
    ) : AnonymousResponse()

    @Schema // Schema here
    data class DiningRoom(
        override val id: Int,
        val hasTable: Boolean,
    ) : AnonymousResponse()
}

which ultimately resulted in the following orval code:

anonymousResponse.ts

import type { AnonymousResponseLounge } from './anonymousResponseLounge';
import type { AnonymousResponseDiningRoom } from './anonymousResponseDiningRoom';
import type { AnonymousResponseOneOf } from './anonymousResponseOneOf';

export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;

anonymousResponseLounge.ts

export interface AnonymousResponseLounge {
  id: number;
  hasTv: boolean;
  type?: string;
}

anonymousResponseDiningRoom.ts

export interface AnonymousResponseLounge {
  id: number;
  hasTv: boolean;
  type?: string;
}

anonymousResponseOneOf.ts

export type AnonymousResponseOneOf = {
  id?: number;
};

And while it doesn't have constant values for the discriminator types, I don't think this is the fault of the spec.