openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG][KOTLIN] Polymorphism not working

Open grassehh opened this issue 1 year ago • 22 comments

Bug Report Checklist

  • [X] Have you provided a full/minimal spec to reproduce the issue?
  • [X] Have you validated the input using an OpenAPI validator (example)?
  • [X] Have you tested with the latest master to confirm the issue still exists?
  • [X] Have you searched for related issues/PRs?
  • [X] What's the actual output vs expected output?
  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

Hi, I'm not certain is this is a bug or a feature since a few things seems to works correctly. When using polymorphism with discriminator exactly as documented on OpenAPI 3.0 specifications, the following of the generated code is incorrect:

  • The parent interface contains all childs properties
  • The child classes do not implement the parent interface
  • The child classes do not have the override keyword on parent interface discriminator property

Note: The same issue is noticeable when using kotlin-spring generator

openapi-generator version

7.4.0

OpenAPI declaration file content or url

https://github.com/grassehh/openapi-generator-experiments/blob/main/openapi.yaml

Generation Details

Checkout this repository and build using gradle build. The generated code is in the build directory.

Steps to reproduce

Simply generate the Kotlin code as explained above then read the generated code in build directory.

Related issues/PRs
Suggest a fix

grassehh avatar Mar 19 '24 17:03 grassehh

@grassehh did you find a work around?.

mike-adonis avatar Aug 19 '24 14:08 mike-adonis

I have used a workaround where I drop the "oneOf" from the parent and leave just the "discriminator". See the Pet, Cat, Dog, Lizard example at https://swagger.io/specification/v3/#discriminator-object

hagis avatar Aug 20 '24 13:08 hagis

I have used a workaround where I drop the "oneOf" from the parent and leave just the "discriminator". See the Pet, Cat, Dog, Lizard example at https://swagger.io/specification/v3/#discriminator-object

The problem I see with this workaround is that your specification is not compliant as the documentation states The discriminator object is legal only when using one of the composite keywords oneOf, anyOf, allOf

@mike-adonis currently our workaround is quite straightforward. We override dataClass.mustache and dataClassReqVar.mustache templates by setting the templateDir variable of the plugin in our gradle.kts file:

templateDir.set("$projectDir/docs/templates")

The drawback is that it requires us to rebase the templates everytime we bump the plugin version.

As of 7.8.0, here are our templates for the kotlin-spring plugin: dataclass.mustache:

/**
* {{{description}}}
{{#vars}}
    * @param {{name}} {{{description}}}
{{/vars}}
*/{{#discriminator}}
    {{>typeInfoAnnotation}}{{/discriminator}}
{{#additionalModelTypeAnnotations}}
    {{{.}}}
{{/additionalModelTypeAnnotations}}
{{#vendorExtensions.x-class-extra-annotation}}
    {{{.}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}(
{{#requiredVars}}
    {{>dataClassReqVar}}{{^-last}},
    {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}},
{{/-last}}{{/optionalVars}}
) {{/discriminator}}{{#vendorExtensions.x-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}}{
{{#discriminator}}{{#model.vendorExtensions.x-discriminator-type}}{{>modelMutable}} {{{discriminator.propertyName}}}: {{{model.vendorExtensions.x-discriminator-type}}}{{/model.vendorExtensions.x-discriminator-type}}{{/discriminator}}
{{#hasEnums}}{{#vars}}{{#isEnum}}
    /**
    * {{{description}}}
    * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
    */
    enum class {{{nameInPascalCase}}}(@get:JsonValue val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) {
    {{#allowableValues}}{{#enumVars}}
        {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}};

    companion object {
    @JvmStatic
    @JsonCreator
    fun forValue(value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}): {{{nameInPascalCase}}} {
    return values().first{it -> it.value == value}
    }
    }
    }
{{/isEnum}}{{/vars}}{{/hasEnums}}
{{#serializableModel}}
    companion object {
    private const val serialVersionUID: kotlin.Long = 1
    }
{{/serializableModel}}
}

dataClassReqVar.mustache:

{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}
    @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
    @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#vendorExtensions.x-field-extra-annotation}}
    {{{.}}}{{/vendorExtensions.x-field-extra-annotation}}
@get:JsonProperty("{{{baseName}}}", required = true){{#vendorExtensions.x-overrides}} override{{/vendorExtensions.x-overrides}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}

Then in the openapi.yaml:

  1. We add x-overrides property in the definition of the properties which are overriden by the child classes. Example:
type: string
x-overrides: true
description: my child property
  1. We add x-implements property in the definition of each subclass. Example:
type: object
x-implements: [ "MyParentClass" ]
description:  My child class
properties:
   ...

grassehh avatar Aug 21 '24 08:08 grassehh

            schema:
              title: Parent
              oneOf:
                - $ref: '#/components/schemas/child1'
                - $ref: '#/components/schemas/child2'
              discriminator:
                propertyName: discriminatorProperty
                mapping:
                  type1: 'child1'
                  type2: 'child2'

fyi. there's a new option called generateOneOfAnyOfWrappers for better oneOf/anyOf support: https://openapi-generator.tech/docs/generators/kotlin/

please give it a try to see if it better meet your requirement

wing328 avatar Aug 21 '24 08:08 wing328

The problem I see with this workaround is that your specification is not compliant as the documentation states The discriminator object is legal only when using one of the composite keywords oneOf, anyOf, allOf

The spec I linked is not mine, it is a sample from the actual OpenAPi 3 documentation.

And the linked spec uses allOf in the child classes meeting the legality constraint for the discriminator object.

hagis avatar Aug 21 '24 08:08 hagis

fyi. there's a new option called generateOneOfAnyOfWrappers for better oneOf/anyOf support: https://openapi-generator.tech/docs/generators/kotlin/

please give it a try to see if it better meet your requirement

I tried that option beginning of June 2024 but the results were not usable. Have there been improvements on it since then?

hagis avatar Aug 21 '24 08:08 hagis

fyi. there's a new option called generateOneOfAnyOfWrappers for better oneOf/anyOf support: https://openapi-generator.tech/docs/generators/kotlin/

please give it a try to see if it better meet your requirement

I just tried the generateOneOfAnyOfWrappers option but it generated uncompilable code. There are errors such as "Unresolved reference 'TypeAdapterFactory'".

hagis avatar Sep 03 '24 04:09 hagis

I'm having the same experience. I'm using kotlinx.serialization and seems like generateOneOfAnyOfWrappers forces GSON, which makes it incompilable.

marekabaffy avatar Sep 19 '24 11:09 marekabaffy

Still no good as of 7.10.0.

Jeaung avatar Dec 20 '24 03:12 Jeaung

I'm running into this as well (7.11.0). I tried enabling configOptions.put("generateOneOfAnyOfWrappers", "true") as @wing328 suggested but this did not change the code generation. My schema is split over multiple files, it looks like this:

item-event_create_v1.yaml

  details:
    description: "Event details."
    oneOf:
      - $ref: "./item-event_create_detail_absolute_v1.yaml"
      - $ref: "./item-event_create_detail_relative_v1.yaml"
      - $ref: "./item-event_create_detail_external_v1.yaml"
      - $ref: "./item-event_create_detail_internal_v1.yaml"
      - $ref: "./item-event_create_detail_exchange_v1.yaml"
    discriminator:
      propertyName: type
      mapping:
        absolute: "item-event_create_detail_absolute_v1"
        relative: "item-event_create_detail_relative_v1"
        external: "item-event_create_detail_external_v1"
        internal: "item-event_create_detail_internal_v1"
        exchange: "item-event_create_detail_exchange_v1"

item-event_create_detail_absolute_v1.yaml

type: object
properties:
  type:
    type: string
  amount:
    $ref: "./amount_v1.yaml"
required: [type, amount]

Same as @grassehh (OP), the generated Swagger / Jackson is pulling every field into the parent class ItemEventCreateV1 and the child classes like ItemEventCreateDetailAbsoluteV1 do not inherit from the parent. ~This by itself would not be an issue because I could decode the type myself, but~ edit at runtime I cannot deserialize an HTTP request because Jackson is complaining about the lack of inheritance.

Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException:
  Could not resolve type id 'absolute' as a subtype of `...models.ItemEventCreateV1Details`:
  Class `...models.ItemEventCreateDetailAbsoluteV1` not subtype of `...models.ItemEventCreateV1Details`

edit this is actually an issue because when I want to create a response the polymorphic output I want to send does not inherit from the parent data type, breaking the compiler.

Argument type mismatch: actual type is '...models.ItemEventDetailAbsoluteV1', but '...models.ItemEventV1Details?' was expected.

I additionally attempted to overwrite the library and serializationLibrary values, but I don't know if that was correct:

    configOptions.put("generateOneOfAnyOfWrappers", "true")
    configOptions.put("library", "jvm-retrofit2")
    configOptions.put("serializationLibrary", "gson")

Doing this produces an error:

Unknown library: jvm-retrofit2
Available libraries:
  spring-boot
  spring-cloud

(I am additionally confused about the Kotlin Generator Docs indicating that the library default is okhttp, which I do not have installed, and I only see spring options from my error above, neither of which are included on the docs page. Is there some interaction going on between spring and the generator?)


I am using Spring Controllers and I just want to enable correct oneOf behavior. Any advice regarding what I am missing?

erikvanderwerf avatar Jan 27 '25 01:01 erikvanderwerf

fyi. there's a new option called generateOneOfAnyOfWrappers for better oneOf/anyOf support: https://openapi-generator.tech/docs/generators/kotlin/ please give it a try to see if it better meet your requirement

I just tried the generateOneOfAnyOfWrappers option but it generated uncompilable code. There are errors such as "Unresolved reference 'TypeAdapterFactory'".

Same; this generates uncompilable code for me...

Additionally, I am not really sure why it's OK to generate an interface that is essentially a super-type. This provides no useful information to downstream users of the client. Why not generate an actual enum? ~~I guess this is maybe a limitation of OpenAPI itself? I would honestly have expected a discriminator to not even be required; clients should be able to say "well, this parses with model B but not model A, so it must be good." I guess the way OpenAPI is currently specified, this isn't possible either?~~

EDIT: I just confirmed that generating a proper enum is exactly what the swift5 and some other generators do, so I think this is just a bug in the Kotlin generator. We could use sealed classes IIRC in Kotlin, where the enum isn't a fully featured sum type.

ianthetechie avatar Jan 27 '25 08:01 ianthetechie

Are you using the kotlin-spring generator? It does not support generateOneOfAnyOfWrappers.

Would be nice to see this feature implemented in kotlin-spring

jsbeckr avatar Mar 26 '25 16:03 jsbeckr

@jsbeckr I was using Kotlin Spring (I've since switched the generator to Java). How do you know that this feature is not available in Kotlin Spring? Is it possible that I could have figured this out by documentation before trying it myself?

erikvanderwerf avatar Mar 26 '25 16:03 erikvanderwerf

@erikvanderwerf The documentation of the the generators is quite sorrow: https://openapi-generator.tech/docs/generators/kotlin-spring

But it's still confusing that every generator supports different features.

jsbeckr avatar Mar 27 '25 08:03 jsbeckr

Are you using the kotlin-spring generator? It does not support generateOneOfAnyOfWrappers.

Would be nice to see this feature implemented in kotlin-spring

No, I'm just using the plain kotlin generator with the jvm-retrofit2 library via a gradle plugin: https://github.com/stadiamaps/stadiamaps-api-kotlin/blob/main/generated-client/build.gradle.kts

ianthetechie avatar Apr 01 '25 05:04 ianthetechie

Same here with 7.14.0-SNAPSHOT (as of May 19th, 2025), with jvm-retrofit and Jackson serialization library: all properties from the sub classes are present in the parent class.

openapi: 3.0.0
info:
  title: Pet Store API
  version: 1.0.0
paths:
  /pets:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
      responses:
        '200':
          description: The response for Pets post operation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'

components:
  schemas:
    Pet:
      required:
        - petType
        - name
        - age
      properties:
        petType:
          type: string
        name:
          type: string
          description: The name of the pet
        age:
          type: integer
          minimum: 0
          description: Age of the pet in years
      discriminator:
        propertyName: petType
        mapping:
          cat: '#/components/schemas/Cat'
          dog: '#/components/schemas/Dog'
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'

    Cat:
      allOf:
        - $ref: '#/components/schemas/Pet'
        - type: object
          required:
            - whiskerLength
          properties:
            whiskerLength:
              type: integer
              minimum: 0
              description: Length of whiskers in centimeters

    Dog:
      allOf:
        - $ref: '#/components/schemas/Pet'
        - type: object
          required:
            - barkVolume
          properties:
            barkVolume:
              type: integer
              minimum: 0
              maximum: 100
              description: Volume of bark in decibels

The generated interface is:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "petType", visible = true)
@JsonSubTypes(
    JsonSubTypes.Type(value = Cat::class, name = "cat"),
    JsonSubTypes.Type(value = Dog::class, name = "dog")
)

interface Pet {

    @get:JsonProperty("petType")
    val petType: kotlin.String
    /* The name of the pet */
    @get:JsonProperty("name")
    val name: kotlin.String
    /* Age of the pet in years */
    @get:JsonProperty("age")
    val age: kotlin.Int
    /* Length of whiskers in centimeters */
    @get:JsonProperty("whiskerLength")
    val whiskerLength: kotlin.Int  // ---> Should only be present in Cat
    /* Volume of bark in decibels */
    @get:JsonProperty("barkVolume")
    val barkVolume: kotlin.Int  // ---> Should only be present in Dog

}

And the generated Cat class has properties from Dog:

data class Cat (

    @get:JsonProperty("petType")
    override val petType: kotlin.String,

    /* The name of the pet */
    @get:JsonProperty("name")
    override val name: kotlin.String,

    /* Age of the pet in years */
    @get:JsonProperty("age")
    override val age: kotlin.Int,

    /* Length of whiskers in centimeters */
    @get:JsonProperty("whiskerLength")
    val whiskerLength: kotlin.Int,

    /* Volume of bark in decibels */
    @get:JsonProperty("barkVolume")
    override val barkVolume: kotlin.Int // ----> Should not be here

) : Pet {


}

njustin avatar May 19 '25 18:05 njustin

Any plans to make this available? We are using the generator for a KMP project with kotlinx.serialization and Ktor.

DDihanov avatar Jun 06 '25 11:06 DDihanov

I have used a workaround where I drop the "oneOf" from the parent and leave just the "discriminator". See the Pet, Cat, Dog, Lizard example at https://swagger.io/specification/v3/#discriminator-object

I'm using the same workaround and it works with both kotlin and kotlin-spring generator. It also seems to be compatible with OAS 3.0.1, https://spec.openapis.org/oas/v3.0.1#discriminator-object , look at the last example

gearhand avatar Jun 24 '25 15:06 gearhand

I am using OpenAPI Generator version 7.14.0, and I am still experiencing issues with proper discriminator handling.

Specifically: When oneOf is dropped from the parent, There is no proper sub class discrimination.(Gson as the serialization library). When oneOf is present in the parent, the discrimination works, but the generated subclasses mix up their properties.

Is there any recommended workaround for this behavior?

Sathsarani371 avatar Jun 30 '25 08:06 Sathsarani371

Not sure about any other workarounds but I have to "vendor" the OpenAPI spec in my Kotlin library and manually edit the spec so it works for now.

ianthetechie avatar Jun 30 '25 08:06 ianthetechie

This issue also exists for the dart, dart-dio, and scala- generators.

I have used a workaround where I drop the "oneOf" from the parent and leave just the "discriminator". See the Pet, Cat, Dog, Lizard example at https://swagger.io/specification/v3/#discriminator-object

Out of the above generators, this workaround only seems to work with dart-dio.

PvtPuddles avatar Sep 04 '25 18:09 PvtPuddles

FYI I set generateOneOfAnyOfWrappers to true using Kotlin version 2.2.21 and generator version7.17.0 with:

library.set("multiplatform")

and the wrappers get successfully generated, even though it says the option is only available for

"Only jvm-retrofit2(library), gson(serializationLibrary) support this option."

and the problem is fixed 🤷‍♂️

DDihanov avatar Dec 03 '25 13:12 DDihanov