Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -663,9 +666,11 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> 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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Original file line number Diff line number Diff line change
@@ -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}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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();

Expand All @@ -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"
);
Expand All @@ -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 ====================
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Expand All @@ -25,5 +26,5 @@ data class Dog(

@field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
val packSize: kotlin.Int = 0
) : Pet()
) : Pet

Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Expand All @@ -25,5 +26,5 @@ data class Dog(

@field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
val packSize: kotlin.Int = 0
) : Pet(petType = petType)
) : Pet

Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}