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 27604bdfca66..7c37611fd878 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 @@ -110,6 +110,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)) @@ -663,9 +666,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 2318e574e45b..8f3fba4f796c 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 @@ -91,7 +91,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 3cdee67e7e61..3a5b072b17a9 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 @@ -322,7 +322,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(); @@ -338,20 +338,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(); @@ -366,35 +366,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 @@ -463,7 +442,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(); @@ -479,14 +458,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" ); @@ -510,12 +487,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 ==================== @@ -627,4 +601,36 @@ public void useTags_false_pathParamOnly_groupsAsDefault() { Assert.assertTrue(groups.containsKey("default")); Assert.assertEquals(co.baseName, "default"); } + + @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 { }