Feature/fix issues - #24961
Feature/fix issues#24961Picazsoo wants to merge 6 commits into
Conversation
…le errors Fixes 4 compile-breaking Kotlin output issues for allOf/discriminator inheritance hierarchies in the kotlin-spring generator: 1. Missing override modifier when a subtype re-declares a property inherited through an allOf-composed parent (AbstractKotlinCodegen now resolves the parent's fully-flattened allOf property set via a new collectAllOfPropertyNames helper instead of only reading the parent schema's direct properties). 2. Discriminator property typed as a narrower per-subtype enum instead of the parent's String type, causing an invalid Kotlin override. The existing discriminator-normalization pass (previously oneOf-only) now also runs for allOf-based discriminator children, fixing the property's dataType/isEnum without altering existing default-value behavior. 3. Free-form/map-typed schemas with a discriminator were rendered as interface X : HashMap<...>(), which Kotlin forbids (an interface cannot extend a class). Map-typed models are now unconditionally excluded from interface promotion and rendered as open class instead. 4. A schema used as an allOf parent by other schemas, but with no discriminator of its own, was emitted as a data class, which Kotlin disallows extending. Added new opt-in additionalProperty ixPolymorphicInheritance (default false) that promotes such models to interface (reusing existing interface/override machinery), gated behind a flag since it changes the generated type shape. Added regression tests and a minimal repro spec (polymorphism-allof-discriminator-inheritance.yaml) covering all 4 issues, verified against kotlinc compilation with the flag on/off. Regenerated affected kotlin-spring samples (whitespace-only diffs from removing dead template code) and docs/generators/kotlin-spring.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pped interfaces
Fixes a Jackson runtime deserialization gap: a model rendered as an
interface (via a genuine own discriminator or via
ixPolymorphicInheritance promotion of an �llOf parent) that is also
named as a value in some discriminator.mapping cannot be instantiated by
Jackson, since @JsonSubTypes would otherwise point at an abstract type.
This adds a synthetic concrete <Schema>Impl data class implementing the
interface whenever the interface's own schema name appears as a
discriminator mapping value anywhere in the document (covering both a
promoted allOf-parent used directly as a payload type, and a
genuinely-discriminated root that maps to itself in its own mapping -
a pre-existing, fix-independent bug). The corresponding @JsonSubTypes
entry is redirected to the Impl class while keeping the wire-format
ame unchanged.
- KotlinSpringServerCodegen.java: compute discriminatorMappedModelNames and
set x-kotlin-poly-impl-needed vendor extension.
- dataClass.mustache: emit the additional {{classname}}Impl data class.
- implClassReqVar.mustache / implClassOptVar.mustache: new partials with
unconditional override for the Impl class constructor properties.
- typeInfoAnnotation.mustache: redirect @JsonSubTypes.Type value to the
Impl class when applicable.
- Extend polymorphism-allof-discriminator-inheritance.yaml regression spec
and add 2 new tests to KotlinSpringServerCodegenTest covering the
self-mapped-root and promoted-interface-referenced-elsewhere cases.
- Update fixPolymorphicInheritance CLI option description and regenerate
docs/generators/kotlin-spring.md.
Verified: full kotlin/kotlin-spring test suite passes (0 failures);
regenerated all 34 bin/configs/kotlin-spring*.yaml samples with no
unintended diffs; repro spec compiles cleanly with kotlinc and Jackson
deserializes correctly at runtime for all previously-failing cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hemas The synthetic `<Schema>Impl` concrete leaf (introduced for interfaces used as discriminator mapping values) previously hardcoded the literal Impl suffix independently in both dataClass.mustache and typeInfoAnnotation.mustache, with no check for whether a schema was already declared under that name in the document. A spec with e.g. both Foo (needing a synthetic leaf) and an unrelated real schema literally named FooImpl would produce a duplicate-class compile error. - KotlinSpringServerCodegen.java: compute a collision-free resolved name once, stored in a new x-kotlin-poly-impl-name vendor extension: try `<classname>Impl`, falling back to `<classname>Impl2`, `Impl3`, ... against the set of all real schema classnames in the document. - dataClass.mustache / typeInfoAnnotation.mustache: reference x-kotlin-poly-impl-name instead of hardcoding the Impl literal, removing the duplicated/hardcoded suffix logic. - Extend polymorphism-allof-discriminator-inheritance.yaml with a deliberately colliding PlaceImpl schema and add a regression test asserting the fallback name (PlaceImpl2) is used and correctly wired into @JsonSubTypes. Verified: full kotlin/kotlin-spring suite passes (276 tests, 0 failures); regenerated regression spec compiles cleanly with kotlinc/Maven; all 34 bin/configs/kotlin-spring*.yaml samples regenerated with no unintended diffs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing)
Previously the synthetic <Schema>Impl concrete leaf class (introduced to
fix a Jackson runtime deserialization gap for interfaces promoted by
fixPolymorphicInheritance, or self-mapped discriminator roots) was
appended as extra content inside the interface's own generated .kt file.
This meant it could not be suppressed or substituted via the standard
schemaMapping/importMapping conventions the way real schemas can.
This change moves the Impl class into its own genuinely separate
generated model file:
- KotlinSpringServerCodegen#postProcessAllModels now builds a synthetic
CodegenModel/ModelMap/ModelsMap for the Impl class and injects it into
the map returned by postProcessAllModels, which is what drives
DefaultGenerator's per-model file-generation loop. This gets
schemaMapping-based suppression for free, since DefaultGenerator's
file-generation loop already checks config.schemaMapping() against
every key in that map, including injected ones.
- Extracted the Impl class's body out of dataClass.mustache into a new
standalone implDataClass.mustache, dispatched from model.mustache via
a new x-kotlin-poly-impl-class vendor extension (mirroring the
existing isEnum/x-is-one-of-interface dispatch pattern). This keeps
dataClass.mustache limited to its original interface/data-class/
open-class-for-map branching, with zero awareness of the Impl
mechanism.
- implClassReqVar.mustache/implClassOptVar.mustache updated to qualify
nested enum-typed properties via {{parent}} instead of {{classname}},
since the Impl class now renders in its own separate model context.
Also documents in the fixPolymorphicInheritance CLI option description
that the synthetic Impl class is generated in its own file and can be
suppressed via schema-mappings.
Verified: full kotlin/kotlin-spring codegen test suite passes (0
failures); regenerated regression spec with the flag enabled compiles
cleanly with kotlinc via a Maven scaffold, with the Impl classes
(including the Phase 3 collision-fallback PlaceImpl2 case) now living in
their own separate files; regenerated all 34 kotlin-spring sample
configs with no unintended diffs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…terfaces gap A model promoted to interface by fixPolymorphicInheritance (allOf parent with children, no own discriminator) that is also a member of a discriminator-free oneOf union (useDeductionForOneOfInterfaces) compiled but failed at runtime: oneof_interface.mustache's deduction @JsonSubTypes block still named the now-abstract interface directly instead of redirecting to the synthetic <Schema>Impl leaf that the existing discriminator-mapping path already uses. - KotlinSpringServerCodegen.java: collect classnames from interfaceModels of every discriminator-free, non-empty oneOf model (deductionOneOfMemberModelNames), union with discriminatorMappedModelNames when computing needsSyntheticImpl. - oneof_interface.mustache: deduction JsonSubTypes entries now redirect to x-kotlin-poly-impl-name when set, mirroring typeInfoAnnotation.mustache. - New regression spec + two tests (flag-on/flag-off) covering the interaction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…criminator inheritance)
Fixes three real-world (TMForum-sourced) kotlin-spring codegen bugs found in addition to
the earlier fixPolymorphicInheritance work. All three are pre-existing, always-on bugs
(reproduced identically regardless of fixPolymorphicInheritance), so the fixes are
unconditional default behavior changes, not gated behind any flag.
- Regression A: an anyOf-single-ref to a discriminated schema (e.g. a synthesized
recursive "map value" inline model) was falsely marked isInherited by the Kotlin
override-detection step even though anyOf never sets a real Kotlin parent, emitting
override with no supertype clause at all. Fixed by guarding that detection step on
m.parent != null in AbstractKotlinCodegen.fromModel.
- Regression B: a oneOf+discriminator "grouping" interface with no properties of its own
(its abstract discriminator property is synthesized purely from
discriminator.propertyName) could be implemented by models that never supply a value for
that property (e.g. because they extend an unrelated allOf base instead), failing to
compile ("does not implement abstract member"). Fixed by synthesizing a computed
(getter-only) override using the model's own entry in the discriminator's mapping as
the literal wire value, scoped precisely to oneof_interface.mustache-rendered interfaces
(x-is-one-of-interface) so genuine allOf discriminator roots (whose discriminator is
handled purely via @JsonTypeInfo/@JsonIgnoreProperties, with no abstract Kotlin member at
all) are left untouched.
- Regression C: an inherited (not redeclared) enum-typed property's nested type reference
was wrongly qualified with the child/composing model's own classname instead of the
model that actually declares the nested enum, producing an unresolved reference. Fixed by
generalizing the discriminator-only enum retargeting into a general mechanism: any
enum-typed property present in a model's requiredVars/optionalVars/allVars but absent
from its own �ars (so it won't render a nested enum there) is retargeted to whichever
candidate model (parent, or any anyOf/allOf/oneOf composition sibling) actually declares
it, via a new x-kotlin-enum-owner vendor extension consumed by the
dataClass/interface Req/OptVar templates.
Added 3 new minimal regression specs and 3 new tests in KotlinSpringServerCodegenTest.
Verified: full kotlin/kotlin-spring codegen test suite (575 tests, 0 failures); the
original combined TMForum-derived repro spec now compiles cleanly end-to-end via
kotlinc/Maven; all 34 kotlin-spring sample configs regenerated with no unintended diffs.
There was a problem hiding this comment.
11 issues found across 23 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/oneof_interface.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache:11">
P1: When a deduction `oneOf` member’s synthetic implementation is supplied through `schemaMapping`, this emits the bare `FooImpl::class` even though `FooImpl.kt` is suppressed. Resolve the synthetic name through the mapping and add the external type import before rendering, or generated Kotlin cannot compile.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/kotlin-spring/implDataClass.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/implDataClass.mustache:10">
P1: When a synthetic discriminator target has no properties, this renders `data class FooImpl()`, which Kotlin rejects because a data class needs a primary-constructor property. Emit `data` only when `hasVars` is true so empty synthetic implementations are regular classes.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/kotlin-spring/implDataClass.mustache:16">
P2: This template opens and closes two nested Mustache sections over the same list `vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides` (the outer one wrapping the `{`, the override block, and the closing `}`; the inner one wrapping just the override declaration). Mustache iterates a list once per element for each section, so when the list contains more than one override, every override is emitted once per outer iteration and the whole `{ … }` body is repeated once per element. A model that implements two `oneOf`+`discriminator` interfaces with distinct discriminator properties produces a broken file with duplicated `override val` declarations and a stray ` {` (compile error). `KotlinSpringServerCodegen.java` builds this extension as an `ArrayList` and adds one entry per distinct discriminator base name, so multi-element lists are reachable. `dataClass.mustache` renders the same extension with a single section and is not affected — drop the redundant inner section here.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/kotlin-spring/implClassOptVar.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-spring/implClassOptVar.mustache:9">
P1: When a synthetic implementation inherits an optional enum property from an allOf parent, this template emits an invalid enum qualifier. Use `x-kotlin-enum-owner` with `parent` as the fallback, matching `dataClassOptVar.mustache`, so the generated implementation references the model that actually declares the nested enum.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/kotlin-spring/implClassOptVar.mustache:10">
P1: When a user enables `openApiNullable`, `postProcessModelsEnum` marks every optional+nullable var with `x-is-jackson-optional-nullable`, and this Impl template then declares `override val x: JsonNullable<T>`. The parent interface, however, declares the same property (from `interfaceOptVar.mustache`, which never wraps in JsonNullable) as a plain `val x: T?`. Overriding a `T?` member with a `JsonNullable<T>` member is a Kotlin override type mismatch and fails to compile. Because `buildSyntheticImplModelsMap` shares the same `CodegenProperty` instances with the interface model, the flag is present on the synthetic leaf whenever it is present on the interface. Match the interface declaration and drop the JsonNullable branch.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java:1130">
P2: When an `allOf` parent is a `$ref` alias (or an alias points through more than one component), this traversal stops at the alias and misses the parent's properties. Follow resolved references recursively before collecting properties, otherwise valid child redefinitions still generate Kotlin without the required `override` modifier.</violation>
</file>
<file name="docs/generators/kotlin-spring.md">
<violation number="1" location="docs/generators/kotlin-spring.md:36">
P3: The description says a promoted schema is generated as an `interface` and 'can no longer be instantiated directly', but that is not true for free-form/map-typed parents: KotlinSpringServerCodegen sets x-kotlin-poly-open-map for wouldBeInterface && isMap models, and dataClass.mustache renders them as an `open class` (still directly instantiable). Users with a map-typed allOf parent will get an instantiable open class, not the non-instantiable interface the docs promise. Consider adding a short caveat (e.g., 'free-form/map-typed schemas are emitted as an open class instead').</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:1600">
P2: The identity-based `ownVars` guard can't tell an own-declared enum property from an inherited one, so the retarget can repoint a model's own enum property at the parent's enum. `CodegenModel.removeAllDuplicatedProperty()` (line 1173) clones every entry via `removeDuplicatedProperty` (`newList.add(cp.clone())`, line 1189), so `m.vars`, `m.requiredVars`, `m.optionalVars` and `m.allVars` hold independent clone objects for the same logical property — the same PR's comment in `AbstractKotlinCodegen.fromModel` states this explicitly. `Collections.newSetFromMap(new IdentityHashMap<>())` therefore only matches the exact instances currently in `cm.vars`; the clones of an own-declared enum property sitting in `requiredVars`/`optionalVars`/`allVars` still pass `.filter(p -> p.isEnum && !ownVars.contains(p))`, and `retargetInheritedEnumPropertyType` (which matches only on `baseName` + `isEnum`, not `isInherited`) then rewrites `dataType`/`datatypeWithEnum` and sets `x-kotlin-enum-owner` to the owner's classname for them. This contradicts the method's own contract ("A no-op if the child redeclares its own enum property under the same name") and, when the child's nested enum differs from the parent's, silently binds the generated property to the wrong enum type; when they are structurally identical the child's nested enum becomes dead code. Use baseName-based exclusion (and/or the documented `isInherited` check) instead of reference identity.</violation>
<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java:1604">
P1: For a multi-level allOf chain, this lookup misses enums inherited by an intermediate composed parent because it examines only `owner.vars`. Traverse the parent/allOf ancestry to find the model that actually declares the enum before retargeting the leaf.</violation>
<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java:2136">
P1: When a synthetic leaf implements an interface inheriting an enum from an allOf ancestor, its enum property points at the immediate interface instead of `x-kotlin-enum-owner`. Make synthetic enum properties use the same declaring-owner qualifier as the interface.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java:7900">
P3: After the schemaMapping suppresses ServiceQualificationImpl.kt, the test only asserts that ServiceQualification.kt still contains the unqualified reference `ServiceQualificationImpl::class` — it never checks how that reference is resolved. DefaultGenerator uses schemaMapping values only as a suppression key (containsKey at DefaultGenerator.java:471/553), and the test-blessed redirect must compile against `com.example.custom.ServiceQualificationImpl`, so ServiceQualification.kt needs `import com.example.custom.ServiceQualificationImpl` (or a fully-qualified @JsonSubTypes value). If the import-emission path uses `modelPackage + name` (org.openapitools.model.ServiceQualificationImpl), the generated file no longer compiles and this test would pass on broken output. Assert the mapped import (e.g. `import com.example.custom.ServiceQualificationImpl`) in ServiceQualification.kt to lock in the compile-usable redirect.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @JsonSubTypes( | ||
| {{#interfaceModels}} | ||
| JsonSubTypes.Type(value = {{classname}}::class){{^-last}},{{/-last}} | ||
| JsonSubTypes.Type(value = {{#vendorExtensions.x-kotlin-poly-impl-needed}}{{vendorExtensions.x-kotlin-poly-impl-name}}{{/vendorExtensions.x-kotlin-poly-impl-needed}}{{^vendorExtensions.x-kotlin-poly-impl-needed}}{{classname}}{{/vendorExtensions.x-kotlin-poly-impl-needed}}::class){{^-last}},{{/-last}} |
There was a problem hiding this comment.
P1: When a deduction oneOf member’s synthetic implementation is supplied through schemaMapping, this emits the bare FooImpl::class even though FooImpl.kt is suppressed. Resolve the synthetic name through the mapping and add the external type import before rendering, or generated Kotlin cannot compile.
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/oneof_interface.mustache, line 11:
<comment>When a deduction `oneOf` member’s synthetic implementation is supplied through `schemaMapping`, this emits the bare `FooImpl::class` even though `FooImpl.kt` is suppressed. Resolve the synthetic name through the mapping and add the external type import before rendering, or generated Kotlin cannot compile.</comment>
<file context>
@@ -8,7 +8,7 @@
@JsonSubTypes(
{{#interfaceModels}}
- JsonSubTypes.Type(value = {{classname}}::class){{^-last}},{{/-last}}
+ JsonSubTypes.Type(value = {{#vendorExtensions.x-kotlin-poly-impl-needed}}{{vendorExtensions.x-kotlin-poly-impl-name}}{{/vendorExtensions.x-kotlin-poly-impl-needed}}{{^vendorExtensions.x-kotlin-poly-impl-needed}}{{classname}}{{/vendorExtensions.x-kotlin-poly-impl-needed}}::class){{^-last}},{{/-last}}
{{/interfaceModels}}
)
</file context>
| * (rather than "{{parent}}Impl") if that default name collides with another schema | ||
| * already declared in the document. | ||
| */ | ||
| data class {{classname}}( |
There was a problem hiding this comment.
P1: When a synthetic discriminator target has no properties, this renders data class FooImpl(), which Kotlin rejects because a data class needs a primary-constructor property. Emit data only when hasVars is true so empty synthetic implementations are regular classes.
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/implDataClass.mustache, line 10:
<comment>When a synthetic discriminator target has no properties, this renders `data class FooImpl()`, which Kotlin rejects because a data class needs a primary-constructor property. Emit `data` only when `hasVars` is true so empty synthetic implementations are regular classes.</comment>
<file context>
@@ -0,0 +1,22 @@
+ * (rather than "{{parent}}Impl") if that default name collides with another schema
+ * already declared in the document.
+ */
+data class {{classname}}(
+{{#requiredVars}}
+{{>implClassReqVar}}{{^-last}},
</file context>
| data class {{classname}}( | |
| {{#hasVars}}data {{/hasVars}}class {{classname}}( |
| @field:JsonInclude(JsonInclude.Include.{{{vendorExtensions.x-jackson-json-include-policy}}}){{/vendorExtensions.x-jackson-json-include-policy}}{{#vendorExtensions.x-has-json-setter-nulls-skip}} | ||
| @field:JsonSetter(nulls = Nulls.SKIP){{/vendorExtensions.x-has-json-setter-nulls-skip}}{{#vendorExtensions.x-has-json-setter-nulls-fail}} | ||
| @field:JsonSetter(nulls = Nulls.FAIL){{/vendorExtensions.x-has-json-setter-nulls-fail}} | ||
| @param:JsonProperty("{{{baseName}}}") |
There was a problem hiding this comment.
P1: When a synthetic implementation inherits an optional enum property from an allOf parent, this template emits an invalid enum qualifier. Use x-kotlin-enum-owner with parent as the fallback, matching dataClassOptVar.mustache, so the generated implementation references the model that actually declares the nested enum.
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/implClassOptVar.mustache, line 9:
<comment>When a synthetic implementation inherits an optional enum property from an allOf parent, this template emits an invalid enum qualifier. Use `x-kotlin-enum-owner` with `parent` as the fallback, matching `dataClassOptVar.mustache`, so the generated implementation references the model that actually declares the nested enum.</comment>
<file context>
@@ -0,0 +1,10 @@
+ @field:JsonInclude(JsonInclude.Include.{{{vendorExtensions.x-jackson-json-include-policy}}}){{/vendorExtensions.x-jackson-json-include-policy}}{{#vendorExtensions.x-has-json-setter-nulls-skip}}
+ @field:JsonSetter(nulls = Nulls.SKIP){{/vendorExtensions.x-has-json-setter-nulls-skip}}{{#vendorExtensions.x-has-json-setter-nulls-fail}}
+ @field:JsonSetter(nulls = Nulls.FAIL){{/vendorExtensions.x-has-json-setter-nulls-fail}}
+ @param:JsonProperty("{{{baseName}}}")
+ @get:JsonProperty("{{{baseName}}}") override {{>modelMutable}} {{{name}}}: {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable<{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{parent}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{parent}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{/vendorExtensions.x-is-jackson-optional-nullable}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable.undefined(){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
\ No newline at end of file
</file context>
| @param:JsonProperty("{{{baseName}}}") | |
| @param:JsonProperty("{{{baseName}}}") | |
| @get:JsonProperty("{{{baseName}}}") override {{>modelMutable}} {{{name}}}: {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable<{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{#vendorExtensions.x-kotlin-enum-owner}}{{vendorExtensions.x-kotlin-enum-owner}}{{/vendorExtensions.x-kotlin-enum-owner}}{{^vendorExtensions.x-kotlin-enum-owner}}{{parent}}{{/vendorExtensions.x-kotlin-enum-owner}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{#vendorExtensions.x-kotlin-enum-owner}}{{vendorExtensions.x-kotlin-enum-owner}}{{/vendorExtensions.x-kotlin-enum-owner}}{{^vendorExtensions.x-kotlin-enum-owner}}{{parent}}{{/vendorExtensions.x-kotlin-enum-owner}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{/vendorExtensions.x-is-jackson-optional-nullable}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable.undefined(){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-jackson-optional-nullable}} |
| implModel.classname = implName; | ||
| implModel.classFilename = implName; | ||
| implModel.parent = interfaceModel.classname; | ||
| implModel.requiredVars = interfaceModel.requiredVars; |
There was a problem hiding this comment.
P1: When a synthetic leaf implements an interface inheriting an enum from an allOf ancestor, its enum property points at the immediate interface instead of x-kotlin-enum-owner. Make synthetic enum properties use the same declaring-owner qualifier as the interface.
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/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java, line 2136:
<comment>When a synthetic leaf implements an interface inheriting an enum from an allOf ancestor, its enum property points at the immediate interface instead of `x-kotlin-enum-owner`. Make synthetic enum properties use the same declaring-owner qualifier as the interface.</comment>
<file context>
@@ -1700,6 +2106,88 @@ public ModelsMap postProcessModelsEnum(ModelsMap objs) {
+ implModel.classname = implName;
+ implModel.classFilename = implName;
+ implModel.parent = interfaceModel.classname;
+ implModel.requiredVars = interfaceModel.requiredVars;
+ implModel.optionalVars = interfaceModel.optionalVars;
+ implModel.vars = interfaceModel.vars;
</file context>
| .forEach(p -> { | ||
| for (String ownerName : candidateOwners) { | ||
| CodegenModel owner = allModelsMap.get(ownerName); | ||
| if (owner == null || owner.vars == null) { |
There was a problem hiding this comment.
P1: For a multi-level allOf chain, this lookup misses enums inherited by an intermediate composed parent because it examines only owner.vars. Traverse the parent/allOf ancestry to find the model that actually declares the enum before retargeting the leaf.
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/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java, line 1604:
<comment>For a multi-level allOf chain, this lookup misses enums inherited by an intermediate composed parent because it examines only `owner.vars`. Traverse the parent/allOf ancestry to find the model that actually declares the enum before retargeting the leaf.</comment>
<file context>
@@ -1507,6 +1526,282 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
+ .forEach(p -> {
+ for (String ownerName : candidateOwners) {
+ CodegenModel owner = allModelsMap.get(ownerName);
+ if (owner == null || owner.vars == null) {
+ continue;
+ }
</file context>
| if (schema == null || !visited.add(schema)) { | ||
| return propertyNames; | ||
| } |
There was a problem hiding this comment.
P2: When an allOf parent is a $ref alias (or an alias points through more than one component), this traversal stops at the alias and misses the parent's properties. Follow resolved references recursively before collecting properties, otherwise valid child redefinitions still generate Kotlin without the required override modifier.
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/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java, line 1130:
<comment>When an `allOf` parent is a `$ref` alias (or an alias points through more than one component), this traversal stops at the alias and misses the parent's properties. Follow resolved references recursively before collecting properties, otherwise valid child redefinitions still generate Kotlin without the required `override` modifier.</comment>
<file context>
@@ -1090,6 +1111,38 @@ public CodegenModel fromModel(String name, Schema schema) {
+ */
+ private Set<String> collectAllOfPropertyNames(Schema<?> schema, Set<Schema<?>> visited) {
+ Set<String> propertyNames = new HashSet<>();
+ if (schema == null || !visited.add(schema)) {
+ return propertyNames;
+ }
</file context>
| if (schema == null || !visited.add(schema)) { | |
| return propertyNames; | |
| } | |
| if (schema == null) { | |
| return propertyNames; | |
| } | |
| if (!visited.add(schema)) { | |
| return propertyNames; | |
| } | |
| Schema<?> resolvedSchema = ModelUtils.getReferencedSchema(this.openAPI, schema); | |
| if (resolvedSchema != schema) { | |
| propertyNames.addAll(collectAllOfPropertyNames(resolvedSchema, visited)); | |
| return propertyNames; | |
| } |
| ) : {{parent}}{{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} { | ||
| {{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} | ||
| override val {{name}}: {{{type}}} | ||
| get() = {{{value}}} | ||
| {{/vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} | ||
| } |
There was a problem hiding this comment.
P2: This template opens and closes two nested Mustache sections over the same list vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides (the outer one wrapping the {, the override block, and the closing }; the inner one wrapping just the override declaration). Mustache iterates a list once per element for each section, so when the list contains more than one override, every override is emitted once per outer iteration and the whole { … } body is repeated once per element. A model that implements two oneOf+discriminator interfaces with distinct discriminator properties produces a broken file with duplicated override val declarations and a stray { (compile error). KotlinSpringServerCodegen.java builds this extension as an ArrayList and adds one entry per distinct discriminator base name, so multi-element lists are reachable. dataClass.mustache renders the same extension with a single section and is not affected — drop the redundant inner section here.
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/implDataClass.mustache, line 16:
<comment>This template opens and closes two nested Mustache sections over the same list `vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides` (the outer one wrapping the `{`, the override block, and the closing `}`; the inner one wrapping just the override declaration). Mustache iterates a list once per element for each section, so when the list contains more than one override, every override is emitted once per outer iteration and the whole `{ … }` body is repeated once per element. A model that implements two `oneOf`+`discriminator` interfaces with distinct discriminator properties produces a broken file with duplicated `override val` declarations and a stray ` {` (compile error). `KotlinSpringServerCodegen.java` builds this extension as an `ArrayList` and adds one entry per distinct discriminator base name, so multi-element lists are reachable. `dataClass.mustache` renders the same extension with a single section and is not affected — drop the redundant inner section here.</comment>
<file context>
@@ -0,0 +1,22 @@
+{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
+{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>implClassOptVar}}{{^-last}},
+{{/-last}}{{/optionalVars}}
+) : {{parent}}{{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} {
+{{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}}
+ override val {{name}}: {{{type}}}
</file context>
| ) : {{parent}}{{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} { | |
| {{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} | |
| override val {{name}}: {{{type}}} | |
| get() = {{{value}}} | |
| {{/vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} | |
| } | |
| ) : {{parent}}{{#vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} { | |
| override val {{name}}: {{{type}}} | |
| get() = {{{value}}} | |
| } | |
| {{/vendorExtensions.x-kotlin-poly-synthetic-discriminator-overrides}} |
| .filter(Objects::nonNull) | ||
| .flatMap(List::stream) | ||
| .distinct() | ||
| .filter(p -> p.isEnum && !ownVars.contains(p)) |
There was a problem hiding this comment.
P2: The identity-based ownVars guard can't tell an own-declared enum property from an inherited one, so the retarget can repoint a model's own enum property at the parent's enum. CodegenModel.removeAllDuplicatedProperty() (line 1173) clones every entry via removeDuplicatedProperty (newList.add(cp.clone()), line 1189), so m.vars, m.requiredVars, m.optionalVars and m.allVars hold independent clone objects for the same logical property — the same PR's comment in AbstractKotlinCodegen.fromModel states this explicitly. Collections.newSetFromMap(new IdentityHashMap<>()) therefore only matches the exact instances currently in cm.vars; the clones of an own-declared enum property sitting in requiredVars/optionalVars/allVars still pass .filter(p -> p.isEnum && !ownVars.contains(p)), and retargetInheritedEnumPropertyType (which matches only on baseName + isEnum, not isInherited) then rewrites dataType/datatypeWithEnum and sets x-kotlin-enum-owner to the owner's classname for them. This contradicts the method's own contract ("A no-op if the child redeclares its own enum property under the same name") and, when the child's nested enum differs from the parent's, silently binds the generated property to the wrong enum type; when they are structurally identical the child's nested enum becomes dead code. Use baseName-based exclusion (and/or the documented isInherited check) instead of reference identity.
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/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java, line 1600:
<comment>The identity-based `ownVars` guard can't tell an own-declared enum property from an inherited one, so the retarget can repoint a model's own enum property at the parent's enum. `CodegenModel.removeAllDuplicatedProperty()` (line 1173) clones every entry via `removeDuplicatedProperty` (`newList.add(cp.clone())`, line 1189), so `m.vars`, `m.requiredVars`, `m.optionalVars` and `m.allVars` hold independent clone objects for the same logical property — the same PR's comment in `AbstractKotlinCodegen.fromModel` states this explicitly. `Collections.newSetFromMap(new IdentityHashMap<>())` therefore only matches the exact instances currently in `cm.vars`; the clones of an own-declared enum property sitting in `requiredVars`/`optionalVars`/`allVars` still pass `.filter(p -> p.isEnum && !ownVars.contains(p))`, and `retargetInheritedEnumPropertyType` (which matches only on `baseName` + `isEnum`, not `isInherited`) then rewrites `dataType`/`datatypeWithEnum` and sets `x-kotlin-enum-owner` to the owner's classname for them. This contradicts the method's own contract ("A no-op if the child redeclares its own enum property under the same name") and, when the child's nested enum differs from the parent's, silently binds the generated property to the wrong enum type; when they are structurally identical the child's nested enum becomes dead code. Use baseName-based exclusion (and/or the documented `isInherited` check) instead of reference identity.</comment>
<file context>
@@ -1507,6 +1526,282 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .distinct()
+ .filter(p -> p.isEnum && !ownVars.contains(p))
+ .forEach(p -> {
+ for (String ownerName : candidateOwners) {
</file context>
| |documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc| | ||
| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| | ||
| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| | ||
| |fixPolymorphicInheritance|Fix compile-breaking Kotlin output for `allOf`/`discriminator` inheritance hierarchies where a schema is used as an `allOf` parent by other schemas but has no `discriminator` of its own. When enabled, such a schema is generated as an `interface` (like a genuinely polymorphic root) instead of a `data class`, which Kotlin does not allow extending. This changes the generated type shape for affected schemas (they can no longer be instantiated directly), so it is opt-in. If a schema promoted to `interface` this way (or a genuinely-discriminated root) is itself named as a value in some `discriminator.mapping` (including a root that maps to itself), a synthetic concrete `<Schema>Impl` data class implementing the interface is also generated (in its own file, named after the resolved class) and substituted into the corresponding `@JsonSubTypes` entry, so Jackson can still construct a concrete instance for that discriminator value. Being a genuinely separate model, this synthetic class can be suppressed via the standard `--schema-mappings` mechanism if you want to substitute your own implementation.| |false| |
There was a problem hiding this comment.
P3: The description says a promoted schema is generated as an interface and 'can no longer be instantiated directly', but that is not true for free-form/map-typed parents: KotlinSpringServerCodegen sets x-kotlin-poly-open-map for wouldBeInterface && isMap models, and dataClass.mustache renders them as an open class (still directly instantiable). Users with a map-typed allOf parent will get an instantiable open class, not the non-instantiable interface the docs promise. Consider adding a short caveat (e.g., 'free-form/map-typed schemas are emitted as an open class instead').
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/generators/kotlin-spring.md, line 36:
<comment>The description says a promoted schema is generated as an `interface` and 'can no longer be instantiated directly', but that is not true for free-form/map-typed parents: KotlinSpringServerCodegen sets x-kotlin-poly-open-map for wouldBeInterface && isMap models, and dataClass.mustache renders them as an `open class` (still directly instantiable). Users with a map-typed allOf parent will get an instantiable open class, not the non-instantiable interface the docs promise. Consider adding a short caveat (e.g., 'free-form/map-typed schemas are emitted as an open class instead').</comment>
<file context>
@@ -33,6 +33,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc|
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original|
|exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true|
+|fixPolymorphicInheritance|Fix compile-breaking Kotlin output for `allOf`/`discriminator` inheritance hierarchies where a schema is used as an `allOf` parent by other schemas but has no `discriminator` of its own. When enabled, such a schema is generated as an `interface` (like a genuinely polymorphic root) instead of a `data class`, which Kotlin does not allow extending. This changes the generated type shape for affected schemas (they can no longer be instantiated directly), so it is opt-in. If a schema promoted to `interface` this way (or a genuinely-discriminated root) is itself named as a value in some `discriminator.mapping` (including a root that maps to itself), a synthetic concrete `<Schema>Impl` data class implementing the interface is also generated (in its own file, named after the resolved class) and substituted into the corresponding `@JsonSubTypes` entry, so Jackson can still construct a concrete instance for that discriminator value. Being a genuinely separate model, this synthetic class can be suppressed via the standard `--schema-mappings` mechanism if you want to substitute your own implementation.| |false|
|generateJsonIncludeAnnotations|Whether to generate policy @JsonInclude annotations on model properties. When true, emits spec-honest annotations (required-field protection and the optional non-nullable policy from optionalNonNullPropertyJsonInclude). When false, none are generated and the global ObjectMapper owns inclusion. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning. A per-property override set via the `x-jackson-json-include-policy` vendor extension is always honored regardless of this flag.| |false|
|generateJsonSetterNullsAnnotations|Whether to generate @JsonSetter(nulls = ...) annotations on optional non-nullable model properties. When true, emits @JsonSetter (Nulls.FAIL when openApiNullable is true, otherwise Nulls.SKIP) so an explicit null in the payload is handled explicitly. When false, none are generated and deserialization null-handling defers to the global ObjectMapper. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning.| |false|
</file context>
| Path serviceQualification = files.get("ServiceQualification.kt").toPath(); | ||
| assertFileContains(serviceQualification, | ||
| "interface ServiceQualification", | ||
| "JsonSubTypes.Type(value = ServiceQualificationImpl::class, name = \"ServiceQualification\")"); |
There was a problem hiding this comment.
P3: After the schemaMapping suppresses ServiceQualificationImpl.kt, the test only asserts that ServiceQualification.kt still contains the unqualified reference ServiceQualificationImpl::class — it never checks how that reference is resolved. DefaultGenerator uses schemaMapping values only as a suppression key (containsKey at DefaultGenerator.java:471/553), and the test-blessed redirect must compile against com.example.custom.ServiceQualificationImpl, so ServiceQualification.kt needs import com.example.custom.ServiceQualificationImpl (or a fully-qualified @JsonSubTypes value). If the import-emission path uses modelPackage + name (org.openapitools.model.ServiceQualificationImpl), the generated file no longer compiles and this test would pass on broken output. Assert the mapped import (e.g. import com.example.custom.ServiceQualificationImpl) in ServiceQualification.kt to lock in the compile-usable redirect.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java, line 7900:
<comment>After the schemaMapping suppresses ServiceQualificationImpl.kt, the test only asserts that ServiceQualification.kt still contains the unqualified reference `ServiceQualificationImpl::class` — it never checks how that reference is resolved. DefaultGenerator uses schemaMapping values only as a suppression key (containsKey at DefaultGenerator.java:471/553), and the test-blessed redirect must compile against `com.example.custom.ServiceQualificationImpl`, so ServiceQualification.kt needs `import com.example.custom.ServiceQualificationImpl` (or a fully-qualified @JsonSubTypes value). If the import-emission path uses `modelPackage + name` (org.openapitools.model.ServiceQualificationImpl), the generated file no longer compiles and this test would pass on broken output. Assert the mapped import (e.g. `import com.example.custom.ServiceQualificationImpl`) in ServiceQualification.kt to lock in the compile-usable redirect.</comment>
<file context>
@@ -7800,4 +7800,354 @@ public void extraImportsDedupAgainstGeneratedImports() throws IOException {
+ Path serviceQualification = files.get("ServiceQualification.kt").toPath();
+ assertFileContains(serviceQualification,
+ "interface ServiceQualification",
+ "JsonSubTypes.Type(value = ServiceQualificationImpl::class, name = \"ServiceQualification\")");
+ assertFileNotContains(serviceQualification, "data class ServiceQualificationImpl(");
+
</file context>
PR checklist
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.
Summary by cubic
Fixes compile-breaking Kotlin output for
allOf/discriminatorinheritance hierarchies in thekotlin-springgenerator, and adds an opt-infixPolymorphicInheritanceflag that promotes non-discriminatedallOfparents tointerfaceso subtypes can legally extend them. Also fixes three pre-existing, always-on compile regressions found in real-world specs.Bug Fixes
allOf-composed parent now get the requiredoverridemodifier.Stringtype forallOfchildren.discriminatornow render asopen classinstead ofinterface : HashMap<...>(), which Kotlin forbids.<Schema>Implleaf so Jackson can construct them.overridewithout a real Kotlin parent.fixPolymorphicInheritanceinterfaces used in deduction-based oneOf unions now redirect@JsonSubTypesto the synthetic Impl leaf.New Features
fixPolymorphicInheritance(default false) renders a non-discriminatedallOfparent as aninterfaceinstead of a finaldata class, so affected schemas can no longer be instantiated directly.<Schema>Impldata class in its own file, named collision-free (Impl,Impl2, ...), suppressible viaschemaMapping.Written for commit 5c8412a. Summary will update on new commits.