From 452982d59af9ea2aab1949457172aae86ecc7be7 Mon Sep 17 00:00:00 2001 From: Matjaz Cuk Date: Tue, 26 May 2026 12:09:57 +0200 Subject: [PATCH 1/2] [kotlin-server] Enable oneOf sealed interface generation The kotlin-server generator produces `typealias Foo = Any` for oneOf schemas with discriminators instead of a proper type hierarchy. This causes compile errors when subtypes try to extend the parent type. Root cause: KotlinServerCodegen declares SchemaSupportFeature.oneOf but does not set useOneOfInterfaces = true, so the codegen pipeline never produces the x-is-one-of-interface vendor extension. Fix (mirrors #23574 for kotlin-spring): - Set useOneOfInterfaces = true in KotlinServerCodegen constructor - Add oneof_interface.mustache template for sealed interface with Jackson annotations - Update model.mustache to route x-is-one-of-interface models to the new template - Update jaxrs-spec model.mustache for the same routing - Fix data_class.mustache: only emit constructor args when x-parent-ctor-args is present - Skip x-parent-ctor-args when useOneOfInterfaces is true (interfaces have no constructors) - Update existing tests to expect sealed interface instead of sealed class for oneOf - Regenerate affected samples --- .../languages/KotlinServerCodegen.java | 11 ++- .../kotlin-server/data_class.mustache | 2 +- .../libraries/jaxrs-spec/model.mustache | 2 +- .../resources/kotlin-server/model.mustache | 2 +- .../kotlin-server/oneof_interface.mustache | 17 ++++ .../kotlin/KotlinServerCodegenTest.java | 94 ++++++++++--------- .../org/openapitools/server/models/Cat.kt | 3 +- .../org/openapitools/server/models/Dog.kt | 3 +- .../org/openapitools/server/models/Pet.kt | 4 +- .../org/openapitools/server/models/Cat.kt | 3 +- .../org/openapitools/server/models/Dog.kt | 3 +- .../org/openapitools/server/models/Pet.kt | 10 +- .../org/openapitools/server/models/Cat.kt | 1 + .../org/openapitools/server/models/Dog.kt | 1 + .../org/openapitools/server/models/Pet.kt | 32 +------ 15 files changed, 95 insertions(+), 93 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index a0ef5c457a2e..17b1256bd1fa 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -121,6 +121,9 @@ public KotlinServerCodegen() { // Enable proper oneOf/anyOf discriminator handling for polymorphism legacyDiscriminatorBehavior = false; + // Generate sealed interfaces for oneOf schemas (mirrors KotlinSpringServerCodegen) + useOneOfInterfaces = true; + modifyFeatureSet(features -> features .includeDocumentationFeatures(DocumentationFeature.Readme) .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML)) @@ -698,9 +701,11 @@ public Map postProcessAllModels(Map objs) childModel.getAllVars().add(discriminatorProp); } - // Set parent constructor args for the discriminator property - childModel.getVendorExtensions().put("x-parent-ctor-args", - discriminatorVarName + " = " + discriminatorVarName); + // Set parent constructor args only when parent is a sealed class (not interface) + if (!useOneOfInterfaces) { + childModel.getVendorExtensions().put("x-parent-ctor-args", + discriminatorVarName + " = " + discriminatorVarName); + } } } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache index 12fa7fceef56..7d8ecc084bf5 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache @@ -104,7 +104,7 @@ sealed class {{classname}} {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, {{/-last}}{{/optionalVars}} -){{#parent}} : {{{.}}}({{#vendorExtensions.x-parent-ctor-args}}{{{.}}}{{/vendorExtensions.x-parent-ctor-args}}){{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/parent}} +){{#parent}} : {{{.}}}{{#vendorExtensions.x-parent-ctor-args}}({{{.}}}){{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/parent}} {{#vendorExtensions.x-has-data-class-body}} { {{/vendorExtensions.x-has-data-class-body}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/model.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/model.mustache index 5025694ec5e1..ae0f259f4a8c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/model.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/model.mustache @@ -9,6 +9,6 @@ import {{javaxPackage}}.validation.Valid {{/useBeanValidation}} {{#models}} {{#model}} -{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{>oneof_model}}{{/oneOf}}{{^oneOf}}{{#isAlias}}typealias {{classname}} = {{{dataType}}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/oneOf}}{{/isEnum}} +{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{#oneOf}}{{>oneof_model}}{{/oneOf}}{{^oneOf}}{{#isAlias}}typealias {{classname}} = {{{dataType}}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/oneOf}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/model.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/model.mustache index 780dd84b97e0..bc9e995b1504 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/model.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/model.mustache @@ -6,6 +6,6 @@ package {{modelPackage}} {{#models}} {{#model}} -{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{>data_class}}{{/isEnum}} +{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>data_class}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache new file mode 100644 index 000000000000..de545671d8b3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache @@ -0,0 +1,17 @@ +/** + * {{{description}}} + */ +{{#discriminator}} +{{>typeInfoAnnotation}} +{{/discriminator}} +{{#additionalModelTypeAnnotations}} +{{{.}}} +{{/additionalModelTypeAnnotations}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{.}}} +{{/vendorExtensions.x-class-extra-annotation}} +sealed interface {{classname}}{{#vendorExtensions.x-kotlin-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} { +{{#discriminator}} + val {{propertyName}}: {{{propertyType}}} +{{/discriminator}} +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 56ace6613ba4..9b11170c0ac2 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -325,7 +325,7 @@ public void givenSchemaObjectPropertyNameContainsDollarSignWhenGenerateThenDolla // ==================== Polymorphism and Discriminator Tests ==================== @Test - public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty() throws IOException { + public void oneOfWithDiscriminator_generatesSealedInterfaceWithDiscriminatorProperty() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); output.deleteOnExit(); @@ -341,20 +341,20 @@ public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; Path petModel = Paths.get(outputPath + "/models/Pet.kt"); - // Pet should be a sealed class with Jackson polymorphism annotations and discriminator property + // Pet should be a sealed interface with Jackson polymorphism annotations and discriminator property assertFileContains( petModel, - "sealed class Pet(", - "open val petType: kotlin.String", + "sealed interface Pet", + "val petType:", "@com.fasterxml.jackson.annotation.JsonTypeInfo", "property = \"petType\"", - "visible = true", "@com.fasterxml.jackson.annotation.JsonSubTypes" ); + assertFileNotContains(petModel, "sealed class", "typealias"); } @Test - public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorProperty() throws IOException { + public void oneOfWithDiscriminator_generatesChildrenImplementingInterface() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); output.deleteOnExit(); @@ -369,35 +369,14 @@ public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorPro String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; - // Cat should have petType as overridden non-nullable String with default value + // Cat and Dog should be data classes (not typealias) Path catModel = Paths.get(outputPath + "/models/Cat.kt"); - assertFileContains( - catModel, - "data class Cat(", - "override val petType: kotlin.String = \"cat\"", - ") : Pet(petType = petType)" - ); - // Should NOT be nullable - assertFileNotContains( - catModel, - "petType: kotlin.String?", - "petType: kotlin.Any" - ); + assertFileContains(catModel, "data class Cat("); + assertFileNotContains(catModel, "typealias", "kotlin.Any"); - // Dog should have petType as overridden non-nullable String with default value Path dogModel = Paths.get(outputPath + "/models/Dog.kt"); - assertFileContains( - dogModel, - "data class Dog(", - "override val petType: kotlin.String = \"dog\"", - ") : Pet(petType = petType)" - ); - // Should NOT be nullable - assertFileNotContains( - dogModel, - "petType: kotlin.String?", - "petType: kotlin.Any" - ); + assertFileContains(dogModel, "data class Dog("); + assertFileNotContains(dogModel, "typealias", "kotlin.Any"); } @Test @@ -466,7 +445,7 @@ public void allOfWithDiscriminator_generatesChildrenWithOverrideProperties() thr } @Test - public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws IOException { + public void polymorphismWithoutDiscriminator_generatesSealedInterface() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); output.deleteOnExit(); @@ -482,14 +461,12 @@ public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; Path petModel = Paths.get(outputPath + "/models/Pet.kt"); - // Without discriminator, Pet should be a regular data class (not sealed) - assertFileContains( - petModel, - "data class Pet(" - ); + // Without discriminator, Pet should be a sealed interface (oneOf) without Jackson annotations + assertFileContains(petModel, "sealed interface Pet"); assertFileNotContains( petModel, "sealed class", + "typealias", "@com.fasterxml.jackson.annotation.JsonTypeInfo", "@com.fasterxml.jackson.annotation.JsonSubTypes" ); @@ -513,12 +490,9 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; Path petModel = Paths.get(outputPath + "/models/Pet.kt"); - // When fixJacksonJsonTypeInfoInheritance is false and parent has no properties, - // visible should be false for oneOf pattern - assertFileContains( - petModel, - "visible = false" - ); + // With useOneOfInterfaces, Pet is a sealed interface. + // When fixJacksonJsonTypeInfoInheritance is false, visible should be false. + assertFileContains(petModel, "sealed interface Pet", "visible = false"); } // ==================== useTags for JAXRS-SPEC ==================== @@ -827,4 +801,36 @@ public void testCommonPathDoesNotShadowOtherTags_jaxrsSpec() throws IOException assertFileContains(tagOneApi, "@Path(\"/foo/bar/one\")", "@Path(\"/foo/bar/two\")"); assertFileContains(tagTwoApi, "@Path(\"/foo/bar/three\")", "@Path(\"/baz/bar/four\")"); } + + @Test(description = "oneOf with discriminator generates sealed interface, not typealias") + public void testOneOfWithDiscriminatorGeneratesSealedInterface() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models"; + + // Pet must be a sealed interface with Jackson annotations, not a typealias + assertFileContains(Paths.get(outputPath + "/Pet.kt"), + "sealed interface Pet", + "@com.fasterxml.jackson.annotation.JsonTypeInfo", + "property = \"petType\"", + "@com.fasterxml.jackson.annotation.JsonSubTypes" + ); + assertFileNotContains(Paths.get(outputPath + "/Pet.kt"), "typealias", "kotlin.Any"); + + // Subtypes must extend the sealed interface + assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat"); + assertFileNotContains(Paths.get(outputPath + "/Cat.kt"), "typealias", "kotlin.Any"); + assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog"); + assertFileNotContains(Paths.get(outputPath + "/Dog.kt"), "typealias", "kotlin.Any"); + } } diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Cat.kt index dd82367937fe..9bbbc8d5dc3f 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet cat * @param huntingSkill The measured skill for hunting * @param petType + * @param name */ data class Cat( /* The measured skill for hunting */ @@ -25,7 +26,7 @@ data class Cat( @field:com.fasterxml.jackson.annotation.JsonProperty("petType") val petType: kotlin.Any? = null -) : Pet() +) : Pet { /** * The measured skill for hunting diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Dog.kt index 1360130bed0a..1b1449d303f1 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet dog * @param petType * @param packSize the size of the pack the dog is from + * @param name */ data class Dog( @@ -25,5 +26,5 @@ data class Dog( @field:com.fasterxml.jackson.annotation.JsonProperty("packSize") val packSize: kotlin.Int = 0 -) : Pet() +) : Pet diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt index 737a74aefcfb..f95c6769d874 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -22,5 +22,7 @@ import org.openapitools.server.models.Dog 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 +sealed interface Pet { + val petType: kotlin.String +} diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt index 99ab4e0f3f50..a42fd5ebb9e9 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet cat * @param huntingSkill The measured skill for hunting * @param petType + * @param name */ data class Cat( /* The measured skill for hunting */ @@ -26,7 +27,7 @@ data class Cat( @field:com.fasterxml.jackson.annotation.JsonProperty("petType") override val petType: kotlin.String = "cat", -) : Pet(petType = petType) +) : Pet { /** * The measured skill for hunting diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt index 9f3cbe860d93..124899b6b2aa 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet dog * @param petType * @param packSize the size of the pack the dog is from + * @param name */ data class Dog( @@ -25,5 +26,5 @@ data class Dog( @field:com.fasterxml.jackson.annotation.JsonProperty("packSize") val packSize: kotlin.Int = 0 -) : Pet(petType = petType) +) : Pet diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt index 30ada8deb372..15a177d8515e 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -16,17 +16,13 @@ import org.openapitools.server.models.Dog /** * A pet - * @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 - -) +sealed interface Pet { + val petType: kotlin.String +} diff --git a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt index 9daf3b5219e7..753b08831e46 100644 --- a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet cat * @param huntingSkill The measured skill for hunting * @param petType + * @param name */ data class Cat( /* The measured skill for hunting */ diff --git a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt index 803468f25c14..c2a4a81bfd02 100644 --- a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -16,6 +16,7 @@ package org.openapitools.server.models * A pet dog * @param packSize the size of the pack the dog is from * @param petType + * @param name */ data class Dog( /* the size of the pack the dog is from */ diff --git a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Pet.kt index 438e3b01ae8d..7efc5568f07d 100644 --- a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -16,37 +16,7 @@ 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"); - } +sealed interface Pet { } From 9056b41248497e6e6db124365416a1ecac49d3c2 Mon Sep 17 00:00:00 2001 From: Matjaz Cuk Date: Thu, 30 Jul 2026 10:26:32 +0200 Subject: [PATCH 2/2] [kotlin-server] Fix compile errors in generated oneOf sealed interfaces Only declare the discriminator on the oneOf sealed interface when the subtypes are actually rendered with 'override' (i.e. when fixJacksonJsonTypeInfoInheritance is enabled). Otherwise the subtypes failed to compile with "'petType' hides member of supertype 'Pet' and needs an 'override' modifier". Emit 'override' for inherited properties in the jaxrs-spec library templates, which hit the same error. Let oneOf members declare the sealed interface as a supertype via x-implements, so a discriminator-less oneOf no longer produces an empty interface that nothing implements. --- .../languages/KotlinServerCodegen.java | 6 +++ .../kotlin-server/data_class.mustache | 8 +++- .../jaxrs-spec/data_class_opt_var.mustache | 2 +- .../jaxrs-spec/data_class_req_var.mustache | 2 +- .../kotlin-server/oneof_interface.mustache | 2 + .../kotlin/KotlinServerCodegenTest.java | 37 +++++++++++++++++-- .../org/openapitools/server/models/Pet.kt | 1 - .../org/openapitools/server/models/Cat.kt | 2 +- .../org/openapitools/server/models/Dog.kt | 2 +- 9 files changed, 53 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 17b1256bd1fa..06fb51bfe383 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -620,6 +620,12 @@ public Map postProcessAllModels(Map objs) owner.getRequiredVars().add(parentDiscriminatorProp); owner.getAllVars().add(parentDiscriminatorProp); + // Children are retyped to kotlin.String and marked inherited below, so they can + // override an abstract declaration on the oneOf interface. Only then may the + // interface declare the discriminator - see oneof_interface.mustache. + owner.getVendorExtensions().put("x-one-of-interface-declares-discriminator", true); + owner.getDiscriminator().getVendorExtensions().put("x-one-of-interface-declares-discriminator", true); + // Parent now has properties (just the discriminator) hasParentProperties = true; diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache index 7d8ecc084bf5..16ce788c4d95 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache @@ -104,7 +104,13 @@ sealed class {{classname}} {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}}, {{/-last}}{{/optionalVars}} -){{#parent}} : {{{.}}}{{#vendorExtensions.x-parent-ctor-args}}({{{.}}}){{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/parent}} +){{#parent}} : {{{.}}}{{#vendorExtensions.x-parent-ctor-args}}({{{.}}}){{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{! no newline +}}{{#vendorExtensions.x-implements}}{{! <- oneOf sealed interfaces this model is a member of + }}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{! no newline + }}{{#-last}}{{#parcelizeModels}}, Parcelable{{/parcelizeModels}}{{#serializableModel}}, Serializable{{/serializableModel}}{{/-last}}{{! no newline +}}{{/vendorExtensions.x-implements}}{{! no newline +}}{{^vendorExtensions.x-implements}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{^parcelizeModels}}{{#serializableModel}}: Serializable {{/serializableModel}}{{/parcelizeModels}}{{#parcelizeModels}}{{#serializableModel}} : Parcelable, Serializable {{/serializableModel}}{{/parcelizeModels}}{{/vendorExtensions.x-implements}}{{! no newline +}}{{/parent}} {{#vendorExtensions.x-has-data-class-body}} { {{/vendorExtensions.x-has-data-class-body}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache index f277f71da441..08eb0262380d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_opt_var.mustache @@ -3,4 +3,4 @@ {{/description}} @JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") -{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache index 6f4647d16e98..55c9276e2430 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/data_class_req_var.mustache @@ -3,4 +3,4 @@ {{/description}} @JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") -{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} \ No newline at end of file +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache index de545671d8b3..ee75ea46f2b9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/oneof_interface.mustache @@ -12,6 +12,8 @@ {{/vendorExtensions.x-class-extra-annotation}} sealed interface {{classname}}{{#vendorExtensions.x-kotlin-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} { {{#discriminator}} +{{#vendorExtensions.x-one-of-interface-declares-discriminator}} val {{propertyName}}: {{{propertyType}}} +{{/vendorExtensions.x-one-of-interface-declares-discriminator}} {{/discriminator}} } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 9b11170c0ac2..543d38ebad9d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -493,6 +493,14 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException // With useOneOfInterfaces, Pet is a sealed interface. // When fixJacksonJsonTypeInfoInheritance is false, visible should be false. assertFileContains(petModel, "sealed interface Pet", "visible = false"); + + // The interface must NOT declare the discriminator here: the children are only retyped to + // kotlin.String and marked inherited (i.e. rendered with `override`) when the Jackson fix is + // enabled. Declaring it anyway makes the subtypes fail to compile with + // "'petType' hides member of supertype 'Pet' and needs an 'override' modifier". + assertFileNotContains(petModel, "val petType"); + assertFileNotContains(Paths.get(outputPath + "/models/Cat.kt"), "override val petType"); + assertFileNotContains(Paths.get(outputPath + "/models/Dog.kt"), "override val petType"); } // ==================== useTags for JAXRS-SPEC ==================== @@ -827,10 +835,33 @@ public void testOneOfWithDiscriminatorGeneratesSealedInterface() throws IOExcept ); assertFileNotContains(Paths.get(outputPath + "/Pet.kt"), "typealias", "kotlin.Any"); - // Subtypes must extend the sealed interface - assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat"); + // Subtypes must extend the sealed interface and override its discriminator + assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat", ") : Pet", "override val petType"); assertFileNotContains(Paths.get(outputPath + "/Cat.kt"), "typealias", "kotlin.Any"); - assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog"); + assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog", ") : Pet", "override val petType"); assertFileNotContains(Paths.get(outputPath + "/Dog.kt"), "typealias", "kotlin.Any"); } + + @Test(description = "oneOf without discriminator generates a sealed interface that subtypes implement") + public void testOneOfWithoutDiscriminatorSubtypesImplementInterface() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, JAVALIN6); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models"; + + // Without a discriminator the interface has no members, so it would be uninhabited unless the + // oneOf members declare it as a supertype via x-implements. + assertFileContains(Paths.get(outputPath + "/Pet.kt"), "sealed interface Pet"); + assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat", ") : Pet"); + assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog", ") : Pet"); + } } diff --git a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt index f95c6769d874..c73eb5f442f7 100644 --- a/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -23,6 +23,5 @@ import org.openapitools.server.models.Dog com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog") ) sealed interface Pet { - val petType: kotlin.String } diff --git a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt index 753b08831e46..f058fff179b6 100644 --- a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -26,7 +26,7 @@ data class Cat( @field:com.fasterxml.jackson.annotation.JsonProperty("petType") val petType: kotlin.Any? = null -) +) : Pet { /** * The measured skill for hunting diff --git a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt index c2a4a81bfd02..4ff87cfbe1fd 100644 --- a/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -26,5 +26,5 @@ data class Dog( @field:com.fasterxml.jackson.annotation.JsonProperty("petType") val petType: kotlin.Any? = null -) +) : Pet