OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.3k stars 6.44k forks source link

[BUG][KOTLIN] Polymorphism not working #18167

Open grassehh opened 5 months ago

grassehh commented 5 months ago

Bug Report Checklist

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:

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
mike-adonis commented 3 weeks ago

@grassehh did you find a work around?.

hagis commented 3 weeks ago

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

grassehh commented 3 weeks ago

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
  2. We add x-implements property in the definition of each subclass. Example:

    type: object
    x-implements: [ "MyParentClass" ]
    description:  My child class
    properties:
    ...
wing328 commented 3 weeks ago
            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

hagis commented 3 weeks ago

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 commented 3 weeks ago

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 commented 1 week ago

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'".