Skip to content

[kotlin-server] Add polymorphism, oneOf and allOf support#22610

Merged
wing328 merged 8 commits intoOpenAPITools:masterfrom
dennisameling:kotlin-server-polymorphism-support
Feb 4, 2026
Merged

[kotlin-server] Add polymorphism, oneOf and allOf support#22610
wing328 merged 8 commits intoOpenAPITools:masterfrom
dennisameling:kotlin-server-polymorphism-support

Conversation

@dennisameling
Copy link
Copy Markdown
Contributor

@dennisameling dennisameling commented Dec 31, 2025

Partially fixes #18167 - the original issue was reported for the kotlin client generator while this PR is for the kotlin-server generator

Before

❌ Jackson annotations for JsonTypeInfo and JsonSubTypes are missing
Pet is a regular data class instead of a sealed class
Pet has all properties in its constructor
Dog and Cat don't inherit Pet

Kotlin sample for Pet, Dog and Cat
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 * @param name 
 * @param petType 
 * @param huntingSkill The measured skill for hunting
 * @param packSize the size of the pack the dog is from
 */
data class Pet(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("name")
    val name: kotlin.String,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any?,
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Pet.HuntingSkill,
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
)
{
    /**
    * The measured skill for hunting
    * Values: clueless,lazy,adventurous,aggressive
    */
    enum class HuntingSkill(val value: kotlin.String){
        clueless("clueless"),
        lazy("lazy"),
        adventurous("adventurous"),
        aggressive("aggressive");
    }
}
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any?,
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
)
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any? = null
)
{
    /**
    * The measured skill for hunting
    * Values: clueless,lazy,adventurous,aggressive
    */
    enum class HuntingSkill(val value: kotlin.String){
        clueless("clueless"),
        lazy("lazy"),
        adventurous("adventurous"),
        aggressive("aggressive");
    }
}

After

✅ Jackson annotations for JsonTypeInfo and JsonSubTypes are present
Pet is a sealed class
Pet doesn't have any properties in its constructor; that's left up to the children
Dog and Cat inherit Pet

Kotlin sample for Pet, Dog and Cat
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 */
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.String = "dog",
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
) : Pet()
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.String = "cat",

) : Pet()
{
    /**
    * The measured skill for hunting
    * Values: clueless,lazy,adventurous,aggressive
    */
    enum class HuntingSkill(val value: kotlin.String){
        clueless("clueless"),
        lazy("lazy"),
        adventurous("adventurous"),
        aggressive("aggressive");
    }
}

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Tagging the Kotlin technical committee members: @karismann (2019/03) @Zomzog (2019/04) @andrewemery (2019/10) @4brunu (2019/11) @yutaka0m (2020/03) @stefankoppier (2022/06) @e5l (2024/10)


Summary by cubic

Adds polymorphism support (oneOf and allOf) to the kotlin-server generator. Models now deserialize correctly with Jackson using sealed parents and proper child inheritance.

  • New Features

    • Generate sealed parent classes for discriminator models with @JsonTypeInfo and @JsonSubTypes.
    • Support oneOf and allOf:
      • oneOf: parent exposes only the discriminator; children override it as non-null with the mapped default.
      • allOf: parent keeps its own properties; children override inherited props and pass them to the parent constructor.
    • New option: fixJacksonJsonTypeInfoInheritance (default true) sets visible=true on @JsonTypeInfo and injects the discriminator property into parent/children for oneOf; set to false to keep the previous behavior.
    • Polymorphism without discriminator: generate regular data classes (no sealed parent or Jackson annotations).
    • Updated Javalin 5/6 templates to mark inherited props as override and to pass parent ctor args.
    • Docs updated to mark polymorphism/oneOf/allOf as supported; added tests, samples, and CI runners.
  • Migration

    • No action required for most users; regenerate to get sealed classes and correct Jackson behavior.
    • To preserve earlier behavior, set additionalProperties.fixJacksonJsonTypeInfoInheritance=false.

Written for commit ffa109b. Summary will update on new commits.

|featureHSTS|Avoid sending content if client already has same content, by checking ETag or LastModified properties.| |true|
|featureMetrics|Enables metrics feature.| |true|
|featureResources|Generates routes in a typed way, for both: constructing URLs and reading the parameters.| |true|
|fixJacksonJsonTypeInfoInheritance|When true (default), ensures Jackson polymorphism works correctly by: (1) always setting visible=true on @JsonTypeInfo, and (2) adding the discriminator property to child models with appropriate default values. When false, visible is only set to true if all children already define the discriminator property.| |true|
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where things get tricky. Let's take this schema as an example:

Pet schema
# https://spec.openapis.org/oas/v3.2.0.html#models-with-polymorphism-support-and-a-discriminator-object
# 4.24.8.8 Models with Polymorphism Support and a Discriminator Object
# The following example extends the example of the previous section by adding a Discriminator Object to the Pet schema. Note that the Discriminator Object is only a hint to the consumer of the API and does not change the validation outcome of the schema.
openapi: 3.1.0
info:
  title: Basic polymorphism example with discriminator
  version: "1.0"
components:
  schemas:
    Pet:
      type: object
      discriminator:
        propertyName: petType
        mapping:
          cat: '#/components/schemas/Cat'
          dog: '#/components/schemas/Dog'
      properties:
        name:
          type: string
      required:
        - name
        - petType
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
    Cat:
      description: A pet cat
      type: object
      properties:
        petType:
          const: 'cat'
        huntingSkill:
          type: string
          description: The measured skill for hunting
          enum:
            - clueless
            - lazy
            - adventurous
            - aggressive
      required:
        - huntingSkill
    Dog:
      description: A pet dog
      type: object
      properties:
        petType:
          const: 'dog'
        packSize:
          type: integer
          format: int32
          description: the size of the pack the dog is from
          default: 0
          minimum: 0
      required:
        - petType
        - packSize

What the spec says about discriminator and petType

The discriminator is only a hint and does not change validation

In section 4.24.8.8, the spec explicitly says:

“the Discriminator Object is only a hint … and does not change the validation outcome of the schema.”

This means discriminator by itself doesn’t automatically “require” petType or enforce constraints.

Does the discriminator property have to be present in the payload?

Yes, if the parent schema requires it

In our Pet schema:

required:
  - name
  - petType

So the schema rules say: a valid Pet instance must contain petType, even if the discriminator is "just a hint." The required constraint is what makes it mandatory.

Also, in the discriminator examples section the spec states:

"The expectation now is that a property with name petType MUST be present in the response payload …"

So: yes, petType must be present in the payload, because the parent schema requires it and the discriminator usage assumes it exists.

openapi-generator behavior - with and without the Jackson fix applied

Without the fix, the generated code looks like this:

Without the fix
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 */
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = false)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any? = null
) : Pet()
{
    /**
    * The measured skill for hunting
    * Values: CLUELESS,LAZY,ADVENTUROUS,AGGRESSIVE
    */
    enum class HuntingSkill(val value: kotlin.String){
        CLUELESS("clueless"),
        LAZY("lazy"),
        ADVENTUROUS("adventurous"),
        AGGRESSIVE("aggressive");
    }
}
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    val petType: kotlin.Any?,
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
) : Pet()

As you can see:
🧐 petType is not a property of the parent/sealed Pet class. While this works, Jackson does require petType in the JSON payload for being able to deserialize, so we might as well add it as a property to the sealed class for convenience. Note that we need visible = true in the JsonTypeInfo annotation for Jackson to populate this value.
🧐 the petType property being present in the children classes (Dog and Cat) now fully depends on whether they're defined in the YAML spec. In this case they are, but it's optional in Cat and required in Dog. And it's nullable in both cases. While the code generator correctly generated code according to spec here, its practical value is highly questionable, because Jackson requires petType to always be provided in order to be able to properly deserialize.

With the fix applied, it looks like this:

With the fix applied
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models

import org.openapitools.server.models.Cat
import org.openapitools.server.models.Dog

/**
 * 
 * @param petType 
 */
@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
)
sealed class Pet(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    open val petType: kotlin.String

)
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet cat
 * @param huntingSkill The measured skill for hunting
 * @param petType 
 */
data class Cat(
    /* The measured skill for hunting */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill")
    val huntingSkill: Cat.HuntingSkill,
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    override val petType: kotlin.String = "cat",

) : Pet(petType = petType)
{
    /**
    * The measured skill for hunting
    * Values: CLUELESS,LAZY,ADVENTUROUS,AGGRESSIVE
    */
    enum class HuntingSkill(val value: kotlin.String){
        CLUELESS("clueless"),
        LAZY("lazy"),
        ADVENTUROUS("adventurous"),
        AGGRESSIVE("aggressive");
    }
}
/**
 * Basic polymorphism example with discriminator
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: 1.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
*/
package org.openapitools.server.models


/**
 * A pet dog
 * @param petType 
 * @param packSize the size of the pack the dog is from
 */
data class Dog(
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("petType")
    override val petType: kotlin.String = "dog",
    /* the size of the pack the dog is from */
    
    @field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
    val packSize: kotlin.Int = 0
) : Pet(petType = petType)

As you can see:
petType is now a required, non-nullable property on the parent/sealed Pet class. Since Jackson already requires it to be set for JsonTypeInfo to be able to deserialize things properly, and visible = true, we can be assured that the property will always be set by Jackson.
✅ The petType property is now present as a required, non-nullable property in the children classes (Dog and Cat). As mentioned above, Jackson already requires this in order to be able to deserialize properly.

This might not be desired behavior in all cases

Let's say I want to re-use the Cat class elsewhere without using the discriminator, it might not be expected that petType is now a required, non-nullable field on the Kotlin data class, because the spec says that it's not required for the Cat model. It's probably not the best example, but I hope you get my train of thought.

For this reason, I wanted to make this behavior configurable. But to be honest, it feels like a bit of an edge case, and we might as well not make it configurable (always applying this fix for Jackson). Curious what y'all think about this.

Comment on lines +72 to +74
@Getter
@Setter
private boolean fixJacksonJsonTypeInfoInheritance = true;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment above about this new config option and its behavior.

Comment on lines +113 to +114
// Enable proper oneOf/anyOf discriminator handling for polymorphism
legacyDiscriminatorBehavior = false;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since oneOf and Polymorphism weren't supported at all in the kotlin-server generator before, I consider this to be a non-breaking change, even though the default is set to true in DefaultCodegen. Let me know if you think otherwise.

Comment on lines +407 to +409
// For libraries that use Jackson, set up parent-child relationships for discriminator children
// This enables proper polymorphism support with @JsonTypeInfo and @JsonSubTypes annotations
if (usesJacksonSerialization()) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kotlin-server generator supports 5 libraries at the time or writing:

Library Serialization library
ktor (Ktor v3) kotlinx.serialization
ktor2 kotlinx.serialization
jaxrs-spec Jackson
javalin5 Jackson
javalin6 Jackson

While ktor defaults to kotlinx.serialization, it also supports Jackson and GSON (not implemented in the openapi-generator AFAIK).

And for Javalin, it defaults to Jackson, but there's instructions to use GSON instead as well.

The kotlin generator has a config option called serializationLibrary, which we might actually want to introduce in the kotlin-server generator as well for consistency (ideally re-using code if possible).

Then, useJacksonSerialization would change from:

/**
     * Returns true if the current library uses Jackson for JSON serialization.
     * This is used to determine if Jackson-specific features like polymorphism annotations should be enabled.
     */
    private boolean usesJacksonSerialization() {
        return Constants.JAVALIN5.equals(library) ||
               Constants.JAVALIN6.equals(library) ||
               Constants.JAXRS_SPEC.equals(library);
    }

... to something that checks the value of the serializationLibrary config option instead. That would also open the door for supporting more serialization libraries in the future if folks want. WDYT?

// When false, we only set visible=true if the parent has properties (allOf pattern)
boolean visibleTrue;

if (fixJacksonJsonTypeInfoInheritance) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual implementation of the Jackson-specific fix as mentioned above.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 67 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="modules/openapi-generator/src/main/resources/kotlin-server/data_class_interface_var.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/data_class_interface_var.mustache:4">
P1: Double nullability marker issue: when a property is both `isNullable=true` and `required=false`, this generates `Type??` which is invalid Kotlin syntax. The `{{^required}}?{{/required}}` should be nested inside a `{{^isNullable}}...{{/isNullable}}` block to prevent adding a second `?` when the type is already nullable.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/libraries/javalin6/data_class_interface_var.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/javalin6/data_class_interface_var.mustache:5">
P1: Potential double nullable marker (`??`) when property is both nullable and not required. If `isNullable=true` and `required=false`, this template generates invalid Kotlin syntax like `Type??`. Consider using a single conditional that checks either condition, e.g., `{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^required}}?{{/required}}{{/isNullable}}`.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@hafthalion
Copy link
Copy Markdown

Nice, thank you! We would also appreciate the fix for this :)

@dennisameling dennisameling force-pushed the kotlin-server-polymorphism-support branch from 6df9076 to 128f30b Compare January 26, 2026 08:05
@dennisameling
Copy link
Copy Markdown
Contributor Author

@wing328 could you help us get this PR merged? Anything you think I should change or do differently? Thanks! 😊

@wing328
Copy link
Copy Markdown
Member

wing328 commented Feb 4, 2026

thanks for the PR

overall it looks good. let's give it a try and see if there are feedback from the community

@wing328 wing328 merged commit 48b7c85 into OpenAPITools:master Feb 4, 2026
46 checks passed
@hafthalion
Copy link
Copy Markdown

Hi, is this actually only for the kotlin-server base generator? We use the jaxrs-spec library of the kotlin-server generator and that overrides data_class.mustache again without sealed class. But thank you anyway!

olsavmic added a commit to olsavmic/openapi-generator that referenced this pull request Mar 21, 2026
…rary

When a oneOf schema has a discriminator, the kotlin-multiplatform generator
now produces a sealed class hierarchy with @JsonClassDiscriminator instead
of a brute-force wrapper that tries each type sequentially. This mirrors
the approach from OpenAPITools#22610 (kotlin-server) adapted for kotlinx.serialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rvlasveld added a commit to rvlasveld/openapi-generator that referenced this pull request Mar 31, 2026
…scriminatorClasses option

Add opt-in `useSealedDiscriminatorClasses` configuration option (default `false`) that enables proper polymorphism support for the kotlin-spring generator using sealed classes instead of interfaces.

When enabled, discriminator parent models are generated as Kotlin sealed classes with proper Jackson `@JsonTypeInfo`/`@JsonSubTypes` annotations. Child classes extend the parent via constructor inheritance with `override` properties. This approach mirrors the polymorphism support added to the kotlin-server generator in OpenAPITools#22610.

The motivation for this change is that the current interface-based polymorphism has several long-standing issues: parent interfaces incorrectly contain all children's properties, child classes don't properly implement the parent interface, the `override` keyword is missing on inherited properties, and multi-level inheritance produces invalid data class hierarchies that don't compile. These issues have been tracked across multiple GitHub issues over several years.

The implementation adds a `postProcessAllModels()` method (ported from `KotlinServerCodegen`) that builds parent-child relationships for discriminator models in three passes: first collecting discriminator mappings, then processing child models to mark inherited properties and generate parent constructor arguments, and finally configuring vendor extensions that control the template rendering.

Both oneOf (type union) and allOf (inheritance) discriminator patterns are supported. For oneOf, the parent sealed class contains only the discriminator property; children get the discriminator as an overridden property with a default value. For allOf, the parent sealed class contains all its declared properties; children override inherited properties and pass them via the parent constructor call.

A companion `fixJacksonJsonTypeInfoInheritance` option (default `true`, only applies when sealed classes are enabled) controls whether Jackson's `visible` flag is set to `true` on `@JsonTypeInfo` and whether discriminator properties are automatically added to child models with appropriate default values.

The default behavior (`useSealedDiscriminatorClasses=false`) is completely unchanged — existing users continue to get interface-based generation with zero sample file changes. This makes the change non-breaking and suitable for a minor release.

New template `dataClassSealedVar.mustache` renders sealed class constructor properties with `open`/`override` modifiers and `@get:Schema`/`@get:JsonProperty` annotations. The existing `dataClass.mustache` and `typeInfoAnnotation.mustache` templates are updated to conditionally render either interface or sealed class based on the flag.

Six new test methods cover oneOf with discriminator, allOf with discriminator, polymorphism without discriminator, and the `fixJacksonJsonTypeInfoInheritance` toggle. All 149 existing tests continue to pass.

Fixes OpenAPITools#18167, OpenAPITools#18206, OpenAPITools#11347, OpenAPITools#8060
Related: OpenAPITools#8366, OpenAPITools#8059

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][KOTLIN] Polymorphism not working

3 participants