[kotlin-spring] Add sealed class polymorphism support#23400
[kotlin-spring] Add sealed class polymorphism support#23400rvlasveld wants to merge 2 commits intoOpenAPITools:masterfrom
Conversation
…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>
|
@cubic-dev-ai review this PR |
@rvlasveld I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
2 issues found across 6 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache:30">
P1: Generated Kotlin child class can fail to compile by extending a sealed/class parent without invoking the parent constructor (`: Parent` instead of `: Parent()`) when `x-parent-ctor-args` is absent.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java:1392">
P1: Model boolean state (`hasVars`/`hasRequired`) is not updated after adding discriminator properties, leaving stale flags and potentially incorrect generated Kotlin model output.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache
Outdated
Show resolved
Hide resolved
...pi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
Show resolved
Hide resolved
Address two issues identified by cubic code review: 1. Always emit parentheses in parent constructor call when `useSealedDiscriminatorClasses` is enabled, even when `x-parent-ctor-args` is absent. This prevents invalid Kotlin like `: Parent` when the parent is a sealed class (should be `: Parent()`). 2. Update `hasVars` and `hasRequired` flags on child models after adding discriminator properties in the oneOf pattern. Without this, a child with no other properties would generate `class Cat(...)` instead of `data class Cat(...)`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both issues identified by cubic are valid and have been fixed in cb7a964: Issue 1 (dataClass.mustache:30): The template now always emits Issue 2 (KotlinSpringServerCodegen.java:1392): Added |
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache:30">
P2: Parent constructor arguments are now emitted only when useSealedDiscriminatorClasses is true, which can break non-sealed inheritance cases where the parent has required constructor parameters and x-parent-ctor-args is set.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| {{/-last}}{{/optionalVars}} | ||
| ){{/discriminator}}{{! no newline | ||
| }}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{! no newline | ||
| }}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{^isMap}}{{#useSealedDiscriminatorClasses}}({{#vendorExtensions.x-parent-ctor-args}}{{{vendorExtensions.x-parent-ctor-args}}}{{/vendorExtensions.x-parent-ctor-args}}){{/useSealedDiscriminatorClasses}}{{/isMap}}{{! no newline |
There was a problem hiding this comment.
P2: Parent constructor arguments are now emitted only when useSealedDiscriminatorClasses is true, which can break non-sealed inheritance cases where the parent has required constructor parameters and x-parent-ctor-args is set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache, line 30:
<comment>Parent constructor arguments are now emitted only when useSealedDiscriminatorClasses is true, which can break non-sealed inheritance cases where the parent has required constructor parameters and x-parent-ctor-args is set.</comment>
<file context>
@@ -27,7 +27,7 @@
{{/-last}}{{/optionalVars}}
){{/discriminator}}{{! no newline
-}}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{^isMap}}{{#vendorExtensions.x-parent-ctor-args}}({{{vendorExtensions.x-parent-ctor-args}}}){{/vendorExtensions.x-parent-ctor-args}}{{/isMap}}{{! no newline
+}}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{^isMap}}{{#useSealedDiscriminatorClasses}}({{#vendorExtensions.x-parent-ctor-args}}{{{vendorExtensions.x-parent-ctor-args}}}{{/vendorExtensions.x-parent-ctor-args}}){{/useSealedDiscriminatorClasses}}{{/isMap}}{{! no newline
}}{{#vendorExtensions.x-kotlin-implements}}, {{{.}}}{{/vendorExtensions.x-kotlin-implements}}{{! <- serializableModel is also handled via x-kotlin-implements
}}{{#vendorExtensions.x-implements-sealed-interfaces}}{{#.}}, {{{.}}}{{/.}}{{/vendorExtensions.x-implements-sealed-interfaces}}{{! <- add sealed interface implementations
</file context>
There was a problem hiding this comment.
Note: this reply is written by Claude (AI), not the PR author directly.
False positive. The x-parent-ctor-args vendor extension is only ever set inside postProcessAllModels(), which returns early when useSealedDiscriminatorClasses is false (line 1168). No other code path sets this extension. So the template guard ({{#useSealedDiscriminatorClasses}}) is logically consistent — both the producer and consumer of x-parent-ctor-args are gated on the same flag.
Re: P2 (dataClass.mustache:30) — This is a false positive. The |
Summary
Add opt-in
useSealedDiscriminatorClassesconfiguration option (defaultfalse) that enables proper polymorphism support for the kotlin-spring generator using sealed classes instead of interfaces.When enabled:
@JsonTypeInfo/@JsonSubTypesannotationsoverridepropertiesfixJacksonJsonTypeInfoInheritanceoption controls Jacksonvisibleflag behaviorThis approach mirrors the polymorphism support added to the kotlin-server generator in #22610.
Default behavior is unchanged
The flag defaults to
false, preserving the existing interface-based generation. Zero sample files change. This is a non-breaking change.Motivation
The current interface-based polymorphism in kotlin-spring has several long-standing issues:
overridekeyword on inherited propertiesRelated Issues
Fixes #18167, #18206, #11347, #8060
Related: #8366, #8059
Related PRs
This PR supersedes the following long-stale open PRs that address subsets of the same problem:
fix(kotlin-spring): add parentCtorCallNeeded model template property(open since Nov 2023)[Kotlin-spring] fix model inheritance(open since Sep 2021)[Kotlin] Fix discriminator interface is not implemented problem(open since Jun 2021)Usage
Example: oneOf with discriminator
Test plan
./bin/generate-samples.sh ./bin/configs/kotlin-spring*) — 0 sample changes./bin/utils/export_docs_generators.sh)🤖 Generated with Claude Code
cc @jimschubert @dr4ke616 @karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier
Summary by cubic
Adds opt‑in sealed class polymorphism to the
kotlin-springgenerator with Jackson-backed discriminators; supports both oneOf and allOf. Default behavior stays the same.New Features
useSealedDiscriminatorClasses(defaultfalse) generates discriminator parents as Kotlin sealed classes and children withoverrideproperties that call the parent constructor.fixJacksonJsonTypeInfoInheritance(defaulttrue) sets@JsonTypeInfo.visiblewhen needed and adds the discriminator property to children with sensible defaults.super. allOf: parent sealed class declares its properties; children override inherited properties and pass them via thesuperconstructor.dataClassSealedVar.mustache;dataClass.mustachenow always emits: Parent()when sealed parents have no ctor args;typeInfoAnnotation.mustachetogglesvisiblebased on context; addedpostProcessAllModels()to build parent/child mappings, mark inherited props, set ctor args, and fixhasVars/hasRequiredfor discriminator-only children. Docs updated (options + oneOf/allOf support), with tests covering oneOf, allOf, no-discriminator, and the visibility toggle.Migration
additionalProperties.useSealedDiscriminatorClasses=true.additionalProperties.fixJacksonJsonTypeInfoInheritance=falseto keepvisible=falseunless all children define the discriminator.Written for commit cb7a964. Summary will update on new commits.