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
5 changes: 5 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/others/kotlin-springboot/**'
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-modelMutable/**'
Expand All @@ -18,6 +19,7 @@ on:
pull_request:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/others/kotlin-springboot/**'
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-modelMutable/**'
Expand All @@ -42,6 +44,9 @@ jobs:
matrix:
sample:
# server
- samples/server/others/kotlin-springboot/oneOf-discriminator
- samples/server/others/kotlin-springboot/oneOf-discriminator-const
- samples/server/others/kotlin-springboot/oneOf-enum-discriminator
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
- samples/server/others/kotlin-server/polymorphism-and-discriminator
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk21.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ on:
push:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/others/kotlin-springboot/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
pull_request:
paths:
- 'samples/server/others/kotlin-server/**'
- 'samples/server/others/kotlin-springboot/**'
- 'samples/server/petstore/kotlin-server/**'
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'

Expand All @@ -23,6 +25,9 @@ jobs:
fail-fast: false
matrix:
sample:
- samples/server/others/kotlin-springboot/oneOf-discriminator
- samples/server/others/kotlin-springboot/oneOf-discriminator-const
- samples/server/others/kotlin-springboot/oneOf-enum-discriminator
- samples/server/others/kotlin-server/polymorphism-allof-and-discriminator
- samples/server/others/kotlin-server/polymorphism-and-discriminator-disabled-jackson-fix
- samples/server/others/kotlin-server/polymorphism-and-discriminator
Expand Down
11 changes: 11 additions & 0 deletions bin/configs/kotlin-spring-boot-oneof-discriminator-const.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: kotlin-spring
outputDir: samples/server/others/kotlin-springboot/oneOf-discriminator-const
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_1/polymorphism-and-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
interfaceOnly: "true"
useSpringBoot3: "true"
11 changes: 11 additions & 0 deletions bin/configs/kotlin-spring-boot-oneof-discriminator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: kotlin-spring
outputDir: samples/server/others/kotlin-springboot/oneOf-discriminator
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism-oneof-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
interfaceOnly: "true"
useSpringBoot3: "true"
11 changes: 11 additions & 0 deletions bin/configs/kotlin-spring-boot-oneof-enum-discriminator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: kotlin-spring
outputDir: samples/server/others/kotlin-springboot/oneOf-enum-discriminator
library: spring-boot
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism-oneof-enum-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
interfaceOnly: "true"
useSpringBoot3: "true"
2 changes: 1 addition & 1 deletion docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|Union|✗|OAS3
|allOf|✗|OAS2,OAS3
|anyOf|✗|OAS3
|oneOf||OAS3
|oneOf||OAS3
|not|✗|OAS3

### Security Feature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,28 @@ public String modelFileFolder() {
}

@Override
@SuppressWarnings("unchecked")
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);
objs = super.updateAllModels(objs);

// Bridge x-implements (set by oneOf pipeline) into x-kotlin-implements (read by Kotlin templates)
for (ModelsMap modelsAttrs : objs.values()) {
for (ModelMap mo : modelsAttrs.getModels()) {
CodegenModel cm = mo.getModel();
List<String> xImplements = (List<String>) cm.getVendorExtensions().get(CodegenConstants.X_IMPLEMENTS);
if (xImplements != null && !xImplements.isEmpty()) {
List<String> kotlinImplements = (List<String>) cm.getVendorExtensions()
.computeIfAbsent(VendorExtension.X_KOTLIN_IMPLEMENTS.getName(), k -> new ArrayList<>());
for (String iface : xImplements) {
if (!kotlinImplements.contains(iface)) {
kotlinImplements.add(iface);
}
}
}
}
}

if (!additionalModelTypeAnnotations.isEmpty()) {
for (String modelName : objs.keySet()) {
Map<String, Object> models = (Map<String, Object>) objs.get(modelName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
Expand Down Expand Up @@ -224,7 +225,8 @@ public KotlinSpringServerCodegen() {
GlobalFeature.ParameterStyling
)
.includeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism
SchemaSupportFeature.Polymorphism,
SchemaSupportFeature.oneOf
)
.includeParameterFeatures(
ParameterFeature.Cookie
Expand All @@ -233,6 +235,9 @@ public KotlinSpringServerCodegen() {

reservedWords.addAll(VARIABLE_RESERVED_WORDS);

// Enable oneOf interface generation (mirrors SpringCodegen behavior)
useOneOfInterfaces = true;

outputFolder = "generated-code/kotlin-spring";
embeddedTemplateDir = templateDir = "kotlin-spring";

Expand Down Expand Up @@ -1328,10 +1333,50 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
}
}

@Override
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
if (additionalProperties.containsKey("jackson")) {
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo", "JsonIgnoreProperties")) {
Map<String, String> oneImport = new HashMap<>();
oneImport.put("import", importMapping.get(i));
if (!imports.contains(oneImport)) {
imports.add(oneImport);
}
}
}
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);

Map<String, CodegenModel> allModelsMap = getAllModels(objs);

// For each oneOf interface with a discriminator, mark the discriminator property
// as inherited in each subtype and set its default value from the discriminator mapping
for (CodegenModel cm : allModelsMap.values()) {
if (Boolean.TRUE.equals(cm.vendorExtensions.get(CodegenConstants.X_IS_ONE_OF_INTERFACE))
&& cm.discriminator != null) {
String discrimBaseName = cm.discriminator.getPropertyBaseName();
String discrimType = cm.discriminator.getPropertyType();
boolean isEnumDiscriminator = cm.discriminator.getIsEnum();

// Build child name -> mapping name lookup from discriminator mappings
Map<String, String> childToMappingName = new HashMap<>();
for (CodegenDiscriminator.MappedModel mm : cm.discriminator.getMappedModels()) {
childToMappingName.put(mm.getModelName(), mm.getMappingName());
}

for (String childName : cm.oneOf) {
CodegenModel child = allModelsMap.get(childName);
if (child != null) {
String mappingName = childToMappingName.get(childName);
markPropertyAsInherited(child, discrimBaseName, discrimType, mappingName, isEnumDiscriminator);
}
}
}
}

if (substituteGenericPagedModel && !pagedModelRegistry.isEmpty()) {
if (getAnnotationLibrary() == AnnotationLibrary.NONE) {
// No @ApiResponse annotations are generated when annotationLibrary=none,
Expand Down Expand Up @@ -1375,6 +1420,47 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
return objs;
}

/**
* Marks the discriminator property as inherited on a subtype and sets its default value
* from the discriminator mapping, so the constructor has the correct default.
*/
private void markPropertyAsInherited(CodegenModel model, String baseName, String dataType,
String discriminatorValue, boolean isEnumDiscriminator) {
Stream.of(model.vars, model.requiredVars, model.optionalVars, model.allVars)
.flatMap(List::stream)
.filter(p -> baseName.equals(p.baseName))
.forEach(p -> {
p.isInherited = true;
// Discriminator properties must match the parent interface type (non-null, required)
if (dataType != null) {
p.dataType = dataType;
p.datatypeWithEnum = dataType;
p.isNullable = false;
p.required = true;
}
if (discriminatorValue != null) {
if (isEnumDiscriminator) {
p.defaultValue = dataType + "." + toEnumVarName(discriminatorValue, dataType);
} else {
p.defaultValue = "\"" + escapeText(discriminatorValue) + "\"";
}
}
});
// Move discriminator property from optionalVars to requiredVars if needed.
// Safe to modify optionalVars here — the stream above has fully completed.
if (dataType != null) {
model.optionalVars.stream()
.filter(p -> baseName.equals(p.baseName))
.findFirst()
.ifPresent(p -> {
model.optionalVars.remove(p);
model.requiredVars.add(p);
});
model.hasRequired = !model.requiredVars.isEmpty();
model.hasOptional = !model.optionalVars.isEmpty();
}
}

@Override
public ModelsMap postProcessModelsEnum(ModelsMap objs) {
objs = super.postProcessModelsEnum(objs);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/**
* {{{description}}}
{{#vars}}
{{#requiredVars}}
* @param {{name}} {{{description}}}
{{/requiredVars}}
{{#optionalVars}}
* @param {{name}} {{{description}}}
{{/vars}}
{{/optionalVars}}
*/{{#discriminator}}
{{>typeInfoAnnotation}}{{/discriminator}}
{{#additionalModelTypeAnnotations}}
Expand Down Expand Up @@ -54,7 +57,7 @@
@JsonCreator
fun forValue(value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}): {{{nameInPascalCase}}} {
return values().firstOrNull{it -> it.value == value}
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{classname}}'")
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ import io.swagger.annotations.ApiModelProperty

{{#models}}
{{#model}}
{{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}}
{{#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}}
{{/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}}
}
Loading
Loading