Skip to content

Commit 68d0a3d

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

132 files changed

Lines changed: 2988 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/**'
@@ -16,6 +17,7 @@ on:
1617
pull_request:
1718
paths:
1819
- 'samples/server/others/kotlin-server/**'
20+
- 'samples/server/others/kotlin-springboot/**'
1921
- 'samples/server/petstore/kotlin-springboot-3*/**'
2022
- 'samples/server/petstore/kotlin-server/**'
2123
- 'samples/server/petstore/kotlin-server-modelMutable/**'
@@ -38,6 +40,9 @@ jobs:
3840
matrix:
3941
sample:
4042
# server
43+
- samples/server/others/kotlin-springboot/oneOf-discriminator
44+
- samples/server/others/kotlin-springboot/oneOf-discriminator-const
45+
- samples/server/others/kotlin-springboot/oneOf-enum-discriminator
4146
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
4247
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
4348
- 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
@@ -310,7 +310,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
310310
|Union|✗|OAS3
311311
|allOf|✗|OAS2,OAS3
312312
|anyOf|✗|OAS3
313-
|oneOf||OAS3
313+
|oneOf||OAS3
314314
|not|✗|OAS3
315315

316316
### 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
@@ -423,10 +423,28 @@ public String modelFileFolder() {
423423
}
424424

425425
@Override
426+
@SuppressWarnings("unchecked")
426427
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
427428
objs = super.postProcessAllModels(objs);
428429
objs = super.updateAllModels(objs);
429430

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

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

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.util.*;
4747
import java.util.regex.Matcher;
4848
import java.util.stream.Collectors;
49+
import java.util.stream.Stream;
4950

5051
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
5152
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -201,7 +202,8 @@ public KotlinSpringServerCodegen() {
201202
GlobalFeature.ParameterStyling
202203
)
203204
.includeSchemaSupportFeatures(
204-
SchemaSupportFeature.Polymorphism
205+
SchemaSupportFeature.Polymorphism,
206+
SchemaSupportFeature.oneOf
205207
)
206208
.includeParameterFeatures(
207209
ParameterFeature.Cookie
@@ -210,6 +212,9 @@ public KotlinSpringServerCodegen() {
210212

211213
reservedWords.addAll(VARIABLE_RESERVED_WORDS);
212214

215+
// Enable oneOf interface generation (mirrors SpringCodegen behavior)
216+
useOneOfInterfaces = true;
217+
213218
outputFolder = "generated-code/kotlin-spring";
214219
embeddedTemplateDir = templateDir = "kotlin-spring";
215220

@@ -1150,6 +1155,94 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
11501155
}
11511156
}
11521157

1158+
@Override
1159+
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
1160+
if (additionalProperties.containsKey("jackson")) {
1161+
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo", "JsonIgnoreProperties")) {
1162+
Map<String, String> oneImport = new HashMap<>();
1163+
oneImport.put("import", importMapping.get(i));
1164+
if (!imports.contains(oneImport)) {
1165+
imports.add(oneImport);
1166+
}
1167+
}
1168+
}
1169+
}
1170+
1171+
@Override
1172+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
1173+
objs = super.postProcessAllModels(objs);
1174+
1175+
Map<String, CodegenModel> allModelsMap = getAllModels(objs);
1176+
1177+
// For each oneOf interface with a discriminator, mark the discriminator property
1178+
// as inherited in each subtype and set its default value from the discriminator mapping
1179+
for (CodegenModel cm : allModelsMap.values()) {
1180+
if (Boolean.TRUE.equals(cm.vendorExtensions.get(CodegenConstants.X_IS_ONE_OF_INTERFACE))
1181+
&& cm.discriminator != null) {
1182+
String discrimBaseName = cm.discriminator.getPropertyBaseName();
1183+
String discrimType = cm.discriminator.getPropertyType();
1184+
boolean isEnumDiscriminator = cm.discriminator.getIsEnum();
1185+
1186+
// Build child name -> mapping name lookup from discriminator mappings
1187+
Map<String, String> childToMappingName = new HashMap<>();
1188+
for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) {
1189+
childToMappingName.put(mm.getModelName(), mm.getMappingName());
1190+
}
1191+
1192+
for (String childName : cm.oneOf) {
1193+
CodegenModel child = allModelsMap.get(childName);
1194+
if (child != null) {
1195+
String mappingName = childToMappingName.get(childName);
1196+
markPropertyAsInherited(child, discrimBaseName, discrimType, mappingName, isEnumDiscriminator);
1197+
}
1198+
}
1199+
}
1200+
}
1201+
1202+
return objs;
1203+
}
1204+
1205+
/**
1206+
* Marks the discriminator property as inherited on a subtype and sets its default value
1207+
* from the discriminator mapping, so the constructor has the correct default.
1208+
*/
1209+
private void markPropertyAsInherited(CodegenModel model, String baseName, String dataType,
1210+
String discriminatorValue, boolean isEnumDiscriminator) {
1211+
Stream.of(model.vars, model.requiredVars, model.optionalVars, model.allVars)
1212+
.flatMap(List::stream)
1213+
.filter(p -> baseName.equals(p.baseName))
1214+
.forEach(p -> {
1215+
p.isInherited = true;
1216+
// Discriminator properties must match the parent interface type (non-null, required)
1217+
if (dataType != null) {
1218+
p.dataType = dataType;
1219+
p.datatypeWithEnum = dataType;
1220+
p.isNullable = false;
1221+
p.required = true;
1222+
}
1223+
if (discriminatorValue != null) {
1224+
if (isEnumDiscriminator) {
1225+
p.defaultValue = dataType + "." + toEnumVarName(discriminatorValue, dataType);
1226+
} else {
1227+
p.defaultValue = "\"" + escapeText(discriminatorValue) + "\"";
1228+
}
1229+
}
1230+
});
1231+
// Move discriminator property from optionalVars to requiredVars if needed.
1232+
// Safe to modify optionalVars here — the stream above has fully completed.
1233+
if (dataType != null) {
1234+
model.optionalVars.stream()
1235+
.filter(p -> baseName.equals(p.baseName))
1236+
.findFirst()
1237+
.ifPresent(p -> {
1238+
model.optionalVars.remove(p);
1239+
model.requiredVars.add(p);
1240+
});
1241+
model.hasRequired = !model.requiredVars.isEmpty();
1242+
model.hasOptional = !model.optionalVars.isEmpty();
1243+
}
1244+
}
1245+
11531246
@Override
11541247
public ModelsMap postProcessModelsEnum(ModelsMap objs) {
11551248
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)