Skip to content

Commit 875f200

Browse files
committed
[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
1 parent 74f7b07 commit 875f200

15 files changed

Lines changed: 95 additions & 93 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public KotlinServerCodegen() {
113113
// Enable proper oneOf/anyOf discriminator handling for polymorphism
114114
legacyDiscriminatorBehavior = false;
115115

116+
// Generate sealed interfaces for oneOf schemas (mirrors KotlinSpringServerCodegen)
117+
useOneOfInterfaces = true;
118+
116119
modifyFeatureSet(features -> features
117120
.includeDocumentationFeatures(DocumentationFeature.Readme)
118121
.wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML))
@@ -691,9 +694,11 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
691694
childModel.getAllVars().add(discriminatorProp);
692695
}
693696

694-
// Set parent constructor args for the discriminator property
695-
childModel.getVendorExtensions().put("x-parent-ctor-args",
696-
discriminatorVarName + " = " + discriminatorVarName);
697+
// Set parent constructor args only when parent is a sealed class (not interface)
698+
if (!useOneOfInterfaces) {
699+
childModel.getVendorExtensions().put("x-parent-ctor-args",
700+
discriminatorVarName + " = " + discriminatorVarName);
701+
}
697702
}
698703
}
699704
}

modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ sealed class {{classname}}
104104
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
105105
{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>data_class_opt_var}}{{^-last}},
106106
{{/-last}}{{/optionalVars}}
107-
){{#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}}
107+
){{#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}}
108108
{{#vendorExtensions.x-has-data-class-body}}
109109
{
110110
{{/vendorExtensions.x-has-data-class-body}}

modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/model.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ import {{javaxPackage}}.validation.Valid
99
{{/useBeanValidation}}
1010
{{#models}}
1111
{{#model}}
12-
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{>oneof_model}}{{/oneOf}}{{^oneOf}}{{#isAlias}}typealias {{classname}} = {{{dataType}}}{{/isAlias}}{{^isAlias}}{{>data_class}}{{/isAlias}}{{/oneOf}}{{/isEnum}}
12+
{{#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}}
1313
{{/model}}
1414
{{/models}}

modules/openapi-generator/src/main/resources/kotlin-server/model.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ package {{modelPackage}}
66

77
{{#models}}
88
{{#model}}
9-
{{#isEnum}}{{>enum_class}}{{/isEnum}}{{^isEnum}}{{>data_class}}{{/isEnum}}
9+
{{#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}}
1010
{{/model}}
1111
{{/models}}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* {{{description}}}
3+
*/
4+
{{#discriminator}}
5+
{{>typeInfoAnnotation}}
6+
{{/discriminator}}
7+
{{#additionalModelTypeAnnotations}}
8+
{{{.}}}
9+
{{/additionalModelTypeAnnotations}}
10+
{{#vendorExtensions.x-class-extra-annotation}}
11+
{{{.}}}
12+
{{/vendorExtensions.x-class-extra-annotation}}
13+
sealed interface {{classname}}{{#vendorExtensions.x-kotlin-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} {
14+
{{#discriminator}}
15+
val {{propertyName}}: {{{propertyType}}}
16+
{{/discriminator}}
17+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ public void givenSchemaObjectPropertyNameContainsDollarSignWhenGenerateThenDolla
325325
// ==================== Polymorphism and Discriminator Tests ====================
326326

327327
@Test
328-
public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty() throws IOException {
328+
public void oneOfWithDiscriminator_generatesSealedInterfaceWithDiscriminatorProperty() throws IOException {
329329
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
330330
output.deleteOnExit();
331331

@@ -341,20 +341,20 @@ public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty
341341
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
342342
Path petModel = Paths.get(outputPath + "/models/Pet.kt");
343343

344-
// Pet should be a sealed class with Jackson polymorphism annotations and discriminator property
344+
// Pet should be a sealed interface with Jackson polymorphism annotations and discriminator property
345345
assertFileContains(
346346
petModel,
347-
"sealed class Pet(",
348-
"open val petType: kotlin.String",
347+
"sealed interface Pet",
348+
"val petType:",
349349
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
350350
"property = \"petType\"",
351-
"visible = true",
352351
"@com.fasterxml.jackson.annotation.JsonSubTypes"
353352
);
353+
assertFileNotContains(petModel, "sealed class", "typealias");
354354
}
355355

356356
@Test
357-
public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorProperty() throws IOException {
357+
public void oneOfWithDiscriminator_generatesChildrenImplementingInterface() throws IOException {
358358
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
359359
output.deleteOnExit();
360360

@@ -369,35 +369,14 @@ public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorPro
369369

370370
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
371371

372-
// Cat should have petType as overridden non-nullable String with default value
372+
// Cat and Dog should be data classes (not typealias)
373373
Path catModel = Paths.get(outputPath + "/models/Cat.kt");
374-
assertFileContains(
375-
catModel,
376-
"data class Cat(",
377-
"override val petType: kotlin.String = \"cat\"",
378-
") : Pet(petType = petType)"
379-
);
380-
// Should NOT be nullable
381-
assertFileNotContains(
382-
catModel,
383-
"petType: kotlin.String?",
384-
"petType: kotlin.Any"
385-
);
374+
assertFileContains(catModel, "data class Cat(");
375+
assertFileNotContains(catModel, "typealias", "kotlin.Any");
386376

387-
// Dog should have petType as overridden non-nullable String with default value
388377
Path dogModel = Paths.get(outputPath + "/models/Dog.kt");
389-
assertFileContains(
390-
dogModel,
391-
"data class Dog(",
392-
"override val petType: kotlin.String = \"dog\"",
393-
") : Pet(petType = petType)"
394-
);
395-
// Should NOT be nullable
396-
assertFileNotContains(
397-
dogModel,
398-
"petType: kotlin.String?",
399-
"petType: kotlin.Any"
400-
);
378+
assertFileContains(dogModel, "data class Dog(");
379+
assertFileNotContains(dogModel, "typealias", "kotlin.Any");
401380
}
402381

403382
@Test
@@ -466,7 +445,7 @@ public void allOfWithDiscriminator_generatesChildrenWithOverrideProperties() thr
466445
}
467446

468447
@Test
469-
public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws IOException {
448+
public void polymorphismWithoutDiscriminator_generatesSealedInterface() throws IOException {
470449
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
471450
output.deleteOnExit();
472451

@@ -482,14 +461,12 @@ public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws
482461
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
483462
Path petModel = Paths.get(outputPath + "/models/Pet.kt");
484463

485-
// Without discriminator, Pet should be a regular data class (not sealed)
486-
assertFileContains(
487-
petModel,
488-
"data class Pet("
489-
);
464+
// Without discriminator, Pet should be a sealed interface (oneOf) without Jackson annotations
465+
assertFileContains(petModel, "sealed interface Pet");
490466
assertFileNotContains(
491467
petModel,
492468
"sealed class",
469+
"typealias",
493470
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
494471
"@com.fasterxml.jackson.annotation.JsonSubTypes"
495472
);
@@ -513,12 +490,9 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException
513490
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
514491
Path petModel = Paths.get(outputPath + "/models/Pet.kt");
515492

516-
// When fixJacksonJsonTypeInfoInheritance is false and parent has no properties,
517-
// visible should be false for oneOf pattern
518-
assertFileContains(
519-
petModel,
520-
"visible = false"
521-
);
493+
// With useOneOfInterfaces, Pet is a sealed interface.
494+
// When fixJacksonJsonTypeInfoInheritance is false, visible should be false.
495+
assertFileContains(petModel, "sealed interface Pet", "visible = false");
522496
}
523497

524498
// ==================== useTags for JAXRS-SPEC ====================
@@ -751,4 +725,36 @@ public void testFloatingPointMultipleOfValidationUsesTolerance() throws IOExcept
751725
"if (intVal % 2 != 0) {"
752726
);
753727
}
728+
729+
@Test(description = "oneOf with discriminator generates sealed interface, not typealias")
730+
public void testOneOfWithDiscriminatorGeneratesSealedInterface() throws IOException {
731+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
732+
output.deleteOnExit();
733+
734+
KotlinServerCodegen codegen = new KotlinServerCodegen();
735+
codegen.setOutputDir(output.getAbsolutePath());
736+
codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC);
737+
738+
new DefaultGenerator().opts(new ClientOptInput()
739+
.openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-and-discriminator.yaml"))
740+
.config(codegen))
741+
.generate();
742+
743+
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models";
744+
745+
// Pet must be a sealed interface with Jackson annotations, not a typealias
746+
assertFileContains(Paths.get(outputPath + "/Pet.kt"),
747+
"sealed interface Pet",
748+
"@com.fasterxml.jackson.annotation.JsonTypeInfo",
749+
"property = \"petType\"",
750+
"@com.fasterxml.jackson.annotation.JsonSubTypes"
751+
);
752+
assertFileNotContains(Paths.get(outputPath + "/Pet.kt"), "typealias", "kotlin.Any");
753+
754+
// Subtypes must extend the sealed interface
755+
assertFileContains(Paths.get(outputPath + "/Cat.kt"), "data class Cat");
756+
assertFileNotContains(Paths.get(outputPath + "/Cat.kt"), "typealias", "kotlin.Any");
757+
assertFileContains(Paths.get(outputPath + "/Dog.kt"), "data class Dog");
758+
assertFileNotContains(Paths.get(outputPath + "/Dog.kt"), "typealias", "kotlin.Any");
759+
}
754760
}

samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Cat.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package org.openapitools.server.models
1616
* A pet cat
1717
* @param huntingSkill The measured skill for hunting
1818
* @param petType
19+
* @param name
1920
*/
2021
data class Cat(
2122
/* The measured skill for hunting */
@@ -25,7 +26,7 @@ data class Cat(
2526

2627
@field:com.fasterxml.jackson.annotation.JsonProperty("petType")
2728
val petType: kotlin.Any? = null
28-
) : Pet()
29+
) : Pet
2930
{
3031
/**
3132
* The measured skill for hunting

samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Dog.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package org.openapitools.server.models
1616
* A pet dog
1717
* @param petType
1818
* @param packSize the size of the pack the dog is from
19+
* @param name
1920
*/
2021
data class Dog(
2122

@@ -25,5 +26,5 @@ data class Dog(
2526

2627
@field:com.fasterxml.jackson.annotation.JsonProperty("packSize")
2728
val packSize: kotlin.Int = 0
28-
) : Pet()
29+
) : Pet
2930

samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix/src/main/kotlin/org/openapitools/server/models/Pet.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ import org.openapitools.server.models.Dog
2222
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"),
2323
com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog")
2424
)
25-
sealed class Pet
25+
sealed interface Pet {
26+
val petType: kotlin.String
27+
}
2628

samples/server/others/kotlin-server/polymorphism-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package org.openapitools.server.models
1616
* A pet cat
1717
* @param huntingSkill The measured skill for hunting
1818
* @param petType
19+
* @param name
1920
*/
2021
data class Cat(
2122
/* The measured skill for hunting */
@@ -26,7 +27,7 @@ data class Cat(
2627
@field:com.fasterxml.jackson.annotation.JsonProperty("petType")
2728
override val petType: kotlin.String = "cat",
2829

29-
) : Pet(petType = petType)
30+
) : Pet
3031
{
3132
/**
3233
* The measured skill for hunting

0 commit comments

Comments
 (0)