Skip to content

Commit 843cec4

Browse files
rkawa01claudebrohaczKZwolski
committed
[KOTLIN-SPRING] Add oneOf sealed interface support with discriminator
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-Authored-By: brohacz <brohacz@users.noreply.github.com> Co-Authored-By: KZwolski <KZwolski@users.noreply.github.com>
1 parent 3971b4f commit 843cec4

132 files changed

Lines changed: 2996 additions & 225 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/samples-kotlin-server-jdk17.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
paths:
66
- 'samples/server/others/kotlin-server/**'
7+
- 'samples/server/others/kotlin-springboot/**'
78
- 'samples/server/petstore/kotlin-springboot-3*/**'
89
- 'samples/server/petstore/kotlin-server/**'
910
- 'samples/server/petstore/kotlin-server-modelMutable/**'
@@ -18,6 +19,7 @@ on:
1819
pull_request:
1920
paths:
2021
- 'samples/server/others/kotlin-server/**'
22+
- 'samples/server/others/kotlin-springboot/**'
2123
- 'samples/server/petstore/kotlin-springboot-3*/**'
2224
- 'samples/server/petstore/kotlin-server/**'
2325
- 'samples/server/petstore/kotlin-server-modelMutable/**'
@@ -42,6 +44,9 @@ jobs:
4244
matrix:
4345
sample:
4446
# server
47+
- samples/server/others/kotlin-springboot/oneOf-discriminator
48+
- samples/server/others/kotlin-springboot/oneOf-discriminator-const
49+
- samples/server/others/kotlin-springboot/oneOf-enum-discriminator
4550
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
4651
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
4752
- samples/server/others/kotlin-server/polymorphism-and-discriminator

.github/workflows/samples-kotlin-server-jdk21.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ on:
44
push:
55
paths:
66
- 'samples/server/others/kotlin-server/**'
7+
- 'samples/server/others/kotlin-springboot/**'
78
- 'samples/server/petstore/kotlin-server/**'
89
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
910
pull_request:
1011
paths:
1112
- 'samples/server/others/kotlin-server/**'
13+
- 'samples/server/others/kotlin-springboot/**'
1214
- 'samples/server/petstore/kotlin-server/**'
1315
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
1416

@@ -23,6 +25,9 @@ jobs:
2325
fail-fast: false
2426
matrix:
2527
sample:
28+
- samples/server/others/kotlin-springboot/oneOf-discriminator
29+
- samples/server/others/kotlin-springboot/oneOf-discriminator-const
30+
- samples/server/others/kotlin-springboot/oneOf-enum-discriminator
2631
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
2732
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
2833
- samples/server/others/kotlin-server/polymorphism-and-discriminator
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: kotlin-spring
2+
outputDir: samples/server/others/kotlin-springboot/oneOf-discriminator-const
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism-and-discriminator.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
interfaceOnly: "true"
11+
useSpringBoot3: "true"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: kotlin-spring
2+
outputDir: samples/server/others/kotlin-springboot/oneOf-discriminator
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism-oneof-discriminator.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
interfaceOnly: "true"
11+
useSpringBoot3: "true"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: kotlin-spring
2+
outputDir: samples/server/others/kotlin-springboot/oneOf-enum-discriminator
3+
library: spring-boot
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism-oneof-enum-discriminator.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
6+
additionalProperties:
7+
documentationProvider: none
8+
annotationLibrary: none
9+
useSwaggerUI: "false"
10+
interfaceOnly: "true"
11+
useSpringBoot3: "true"

docs/generators/kotlin-spring.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
313313
|Union|✗|OAS3
314314
|allOf|✗|OAS2,OAS3
315315
|anyOf|✗|OAS3
316-
|oneOf||OAS3
316+
|oneOf||OAS3
317317
|not|✗|OAS3
318318

319319
### Security Feature

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,10 +421,28 @@ public String modelFileFolder() {
421421
}
422422

423423
@Override
424+
@SuppressWarnings("unchecked")
424425
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
425426
objs = super.postProcessAllModels(objs);
426427
objs = super.updateAllModels(objs);
427428

429+
// Bridge x-implements (set by oneOf pipeline) into x-kotlin-implements (read by Kotlin templates)
430+
for (ModelsMap modelsAttrs : objs.values()) {
431+
for (ModelMap mo : modelsAttrs.getModels()) {
432+
CodegenModel cm = mo.getModel();
433+
List<String> xImplements = (List<String>) cm.getVendorExtensions().get(CodegenConstants.X_IMPLEMENTS);
434+
if (xImplements != null && !xImplements.isEmpty()) {
435+
List<String> kotlinImplements = (List<String>) cm.getVendorExtensions()
436+
.computeIfAbsent(VendorExtension.X_KOTLIN_IMPLEMENTS.getName(), k -> new ArrayList<>());
437+
for (String iface : xImplements) {
438+
if (!kotlinImplements.contains(iface)) {
439+
kotlinImplements.add(iface);
440+
}
441+
}
442+
}
443+
}
444+
}
445+
428446
if (!additionalModelTypeAnnotations.isEmpty()) {
429447
for (String modelName : objs.keySet()) {
430448
Map<String, Object> models = (Map<String, Object>) objs.get(modelName);

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.util.*;
5050
import java.util.regex.Matcher;
5151
import java.util.stream.Collectors;
52+
import java.util.stream.Stream;
5253

5354
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
5455
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -224,7 +225,8 @@ public KotlinSpringServerCodegen() {
224225
GlobalFeature.ParameterStyling
225226
)
226227
.includeSchemaSupportFeatures(
227-
SchemaSupportFeature.Polymorphism
228+
SchemaSupportFeature.Polymorphism,
229+
SchemaSupportFeature.oneOf
228230
)
229231
.includeParameterFeatures(
230232
ParameterFeature.Cookie
@@ -233,6 +235,9 @@ public KotlinSpringServerCodegen() {
233235

234236
reservedWords.addAll(VARIABLE_RESERVED_WORDS);
235237

238+
// Enable oneOf interface generation (mirrors SpringCodegen behavior)
239+
useOneOfInterfaces = true;
240+
236241
outputFolder = "generated-code/kotlin-spring";
237242
embeddedTemplateDir = templateDir = "kotlin-spring";
238243

@@ -1328,10 +1333,50 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
13281333
}
13291334
}
13301335

1336+
@Override
1337+
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
1338+
if (additionalProperties.containsKey("jackson")) {
1339+
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo", "JsonIgnoreProperties")) {
1340+
Map<String, String> oneImport = new HashMap<>();
1341+
oneImport.put("import", importMapping.get(i));
1342+
if (!imports.contains(oneImport)) {
1343+
imports.add(oneImport);
1344+
}
1345+
}
1346+
}
1347+
}
1348+
13311349
@Override
13321350
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
13331351
objs = super.postProcessAllModels(objs);
13341352

1353+
Map<String, CodegenModel> allModelsMap = getAllModels(objs);
1354+
1355+
// For each oneOf interface with a discriminator, mark the discriminator property
1356+
// as inherited in each subtype and set its default value from the discriminator mapping
1357+
for (CodegenModel cm : allModelsMap.values()) {
1358+
if (Boolean.TRUE.equals(cm.vendorExtensions.get(CodegenConstants.X_IS_ONE_OF_INTERFACE))
1359+
&& cm.discriminator != null) {
1360+
String discrimBaseName = cm.discriminator.getPropertyBaseName();
1361+
String discrimType = cm.discriminator.getPropertyType();
1362+
boolean isEnumDiscriminator = cm.discriminator.getIsEnum();
1363+
1364+
// Build child name -> mapping name lookup from discriminator mappings
1365+
Map<String, String> childToMappingName = new HashMap<>();
1366+
for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) {
1367+
childToMappingName.put(mm.getModelName(), mm.getMappingName());
1368+
}
1369+
1370+
for (String childName : cm.oneOf) {
1371+
CodegenModel child = allModelsMap.get(childName);
1372+
if (child != null) {
1373+
String mappingName = childToMappingName.get(childName);
1374+
markPropertyAsInherited(child, discrimBaseName, discrimType, mappingName, isEnumDiscriminator);
1375+
}
1376+
}
1377+
}
1378+
}
1379+
13351380
if (substituteGenericPagedModel && !pagedModelRegistry.isEmpty()) {
13361381
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
13371382
// No @ApiResponse annotations are generated when annotationLibrary=none,
@@ -1375,6 +1420,47 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
13751420
return objs;
13761421
}
13771422

1423+
/**
1424+
* Marks the discriminator property as inherited on a subtype and sets its default value
1425+
* from the discriminator mapping, so the constructor has the correct default.
1426+
*/
1427+
private void markPropertyAsInherited(CodegenModel model, String baseName, String dataType,
1428+
String discriminatorValue, boolean isEnumDiscriminator) {
1429+
Stream.of(model.vars, model.requiredVars, model.optionalVars, model.allVars)
1430+
.flatMap(List::stream)
1431+
.filter(p -> baseName.equals(p.baseName))
1432+
.forEach(p -> {
1433+
p.isInherited = true;
1434+
// Discriminator properties must match the parent interface type (non-null, required)
1435+
if (dataType != null) {
1436+
p.dataType = dataType;
1437+
p.datatypeWithEnum = dataType;
1438+
p.isNullable = false;
1439+
p.required = true;
1440+
}
1441+
if (discriminatorValue != null) {
1442+
if (isEnumDiscriminator) {
1443+
p.defaultValue = dataType + "." + toEnumVarName(discriminatorValue, dataType);
1444+
} else {
1445+
p.defaultValue = "\"" + escapeText(discriminatorValue) + "\"";
1446+
}
1447+
}
1448+
});
1449+
// Move discriminator property from optionalVars to requiredVars if needed.
1450+
// Safe to modify optionalVars here — the stream above has fully completed.
1451+
if (dataType != null) {
1452+
model.optionalVars.stream()
1453+
.filter(p -> baseName.equals(p.baseName))
1454+
.findFirst()
1455+
.ifPresent(p -> {
1456+
model.optionalVars.remove(p);
1457+
model.requiredVars.add(p);
1458+
});
1459+
model.hasRequired = !model.requiredVars.isEmpty();
1460+
model.hasOptional = !model.optionalVars.isEmpty();
1461+
}
1462+
}
1463+
13781464
@Override
13791465
public ModelsMap postProcessModelsEnum(ModelsMap objs) {
13801466
objs = super.postProcessModelsEnum(objs);

modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
22
* {{{description}}}
3-
{{#vars}}
3+
{{#requiredVars}}
4+
* @param {{name}} {{{description}}}
5+
{{/requiredVars}}
6+
{{#optionalVars}}
47
* @param {{name}} {{{description}}}
5-
{{/vars}}
8+
{{/optionalVars}}
69
*/{{#discriminator}}
710
{{>typeInfoAnnotation}}{{/discriminator}}
811
{{#additionalModelTypeAnnotations}}
@@ -54,7 +57,7 @@
5457
@JsonCreator
5558
fun forValue(value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}): {{{nameInPascalCase}}} {
5659
return values().firstOrNull{it -> it.value == value}
57-
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{classname}}'")
60+
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'")
5861
}
5962
}
6063
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ import io.swagger.annotations.ApiModelProperty
2323

2424
{{#models}}
2525
{{#model}}
26-
{{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}}
26+
{{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>dataClass}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
2727
{{/model}}
2828
{{/models}}

0 commit comments

Comments
 (0)