diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index 6c43046fbfb9..d9a28aaf1391 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -34,6 +34,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |documentationProvider|Select the OpenAPI documentation provider.|
**none**
Do not publish an OpenAPI specification.
**source**
Publish the original input OpenAPI specification.
**springdoc**
Generate an OpenAPI 3 specification using SpringDoc.
|springdoc| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| +|fixJacksonJsonTypeInfoInheritance|Only applies when useSealedDiscriminatorClasses is true. When true (default), ensures Jackson polymorphism works correctly by: (1) always setting visible=true on @JsonTypeInfo, and (2) adding the discriminator property to child models with appropriate default values. When false, visible is only set to true if all children already define the discriminator property.| |true| |gradleBuildFile|generate a gradle build file using the Kotlin DSL| |true| |groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools| |includeHttpRequestContext|Whether to include HttpServletRequest (blocking) or ServerWebExchange (reactive) as additional parameter in generated methods.| |false| @@ -61,6 +62,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true| |useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled. Incompatible with `openApiNullable`.| |false| |useResponseEntity|Whether (when false) to return actual type (e.g. List<Fruit>) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity<List<Fruit>>). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition| |true| +|useSealedDiscriminatorClasses|Generate sealed classes instead of interfaces for discriminator models. When true, discriminator parent models are generated as sealed classes with proper Jackson @JsonTypeInfo/@JsonSubTypes annotations and child classes extend the parent with constructor inheritance. When false (default), the legacy interface-based polymorphism is used.| |false| |useSealedResponseInterfaces|Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)| |false| |useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. Enabling this option will also enable `useJakartaEe`.| |false| @@ -307,9 +309,9 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Composite|✓|OAS2,OAS3 |Polymorphism|✓|OAS2,OAS3 |Union|✗|OAS3 -|allOf|✗|OAS2,OAS3 +|allOf|✓|OAS2,OAS3 |anyOf|✗|OAS3 -|oneOf|✗|OAS3 +|oneOf|✓|OAS3 |not|✗|OAS3 ### Security Feature diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 6730dd636649..282d7f63142b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -166,6 +166,25 @@ public String getDescription() { @Setter private boolean autoXSpringPaginated = false; @Setter private boolean useSealedResponseInterfaces = false; @Setter private boolean companionObject = false; + @Getter @Setter + private boolean useSealedDiscriminatorClasses = false; + @Getter @Setter + private boolean fixJacksonJsonTypeInfoInheritance = true; + + public static final String USE_SEALED_DISCRIMINATOR_CLASSES = "useSealedDiscriminatorClasses"; + public static final String USE_SEALED_DISCRIMINATOR_CLASSES_DESC = + "Generate sealed classes instead of interfaces for discriminator models. " + + "When true, discriminator parent models are generated as sealed classes with proper " + + "Jackson @JsonTypeInfo/@JsonSubTypes annotations and child classes extend the parent " + + "with constructor inheritance. When false (default), the legacy interface-based " + + "polymorphism is used."; + public static final String FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE = "fixJacksonJsonTypeInfoInheritance"; + public static final String FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC = + "Only applies when useSealedDiscriminatorClasses is true. " + + "When true (default), ensures Jackson polymorphism works correctly by: " + + "(1) always setting visible=true on @JsonTypeInfo, and (2) adding the discriminator property " + + "to child models with appropriate default values. When false, visible is only set to true if " + + "all children already define the discriminator property."; @Getter @Setter protected boolean useSpringBoot3 = false; @@ -201,6 +220,8 @@ public KotlinSpringServerCodegen() { GlobalFeature.ParameterStyling ) .includeSchemaSupportFeatures( + SchemaSupportFeature.allOf, + SchemaSupportFeature.oneOf, SchemaSupportFeature.Polymorphism ) .includeParameterFeatures( @@ -273,6 +294,8 @@ public KotlinSpringServerCodegen() { addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map"); addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated); addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); + addSwitch(USE_SEALED_DISCRIMINATOR_CLASSES, USE_SEALED_DISCRIMINATOR_CLASSES_DESC, useSealedDiscriminatorClasses); + addSwitch(FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC, fixJacksonJsonTypeInfoInheritance); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, "Spring-Cloud-Feign client with Spring-Boot auto-configured settings."); @@ -534,6 +557,23 @@ public void processOpts() { additionalProperties.put(COMPANION_OBJECT, companionObject); } + if (additionalProperties.containsKey(USE_SEALED_DISCRIMINATOR_CLASSES)) { + this.setUseSealedDiscriminatorClasses( + Boolean.parseBoolean(additionalProperties.get(USE_SEALED_DISCRIMINATOR_CLASSES).toString())); + } + additionalProperties.put(USE_SEALED_DISCRIMINATOR_CLASSES, useSealedDiscriminatorClasses); + + if (useSealedDiscriminatorClasses) { + // Enable proper oneOf/anyOf discriminator handling for sealed class polymorphism + legacyDiscriminatorBehavior = false; + } + + if (additionalProperties.containsKey(FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE)) { + this.setFixJacksonJsonTypeInfoInheritance( + Boolean.parseBoolean(additionalProperties.get(FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE).toString())); + } + additionalProperties.put(FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, fixJacksonJsonTypeInfoInheritance); + additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda()); // Set basePackage from invokerPackage @@ -1123,6 +1163,260 @@ public void preprocessOpenAPI(OpenAPI openAPI) { // TODO: Handle tags } + @Override + public Map postProcessAllModels(Map objs) { + objs = super.postProcessAllModels(objs); + + if (!useSealedDiscriminatorClasses) { + return objs; + } + + // Build a map of model name -> model for easy lookup + Map allModelsMap = new HashMap<>(); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + allModelsMap.put(model.getClassname(), model); + } + } + + // First pass: collect all discriminator parent -> children mappings + // Also identify the "true" discriminator owners (not inherited via allOf) + Map childToParentMap = new HashMap<>(); + Set trueDiscriminatorOwners = new HashSet<>(); + + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + if (model.getDiscriminator() != null && model.getDiscriminator().getMappedModels() != null + && !model.getDiscriminator().getMappedModels().isEmpty()) { + String discriminatorPropBaseName = model.getDiscriminator().getPropertyBaseName(); + + for (CodegenDiscriminator.MappedModel mappedModel : model.getDiscriminator().getMappedModels()) { + childToParentMap.put(mappedModel.getModelName(), model.getClassname()); + + // If the mapping name equals the model name, check if we can derive + // a better mapping name from the child's discriminator property enum value + if (mappedModel.getMappingName().equals(mappedModel.getModelName())) { + CodegenModel childModel = allModelsMap.get(mappedModel.getModelName()); + if (childModel != null) { + for (CodegenProperty prop : childModel.getAllVars()) { + if (prop.getBaseName().equals(discriminatorPropBaseName) && prop.isEnum) { + Map allowableValues = prop.getAllowableValues(); + if (allowableValues != null && allowableValues.containsKey("values")) { + @SuppressWarnings("unchecked") + List values = (List) allowableValues.get("values"); + if (values != null && values.size() == 1) { + mappedModel.setMappingName(String.valueOf(values.get(0))); + } + } + } + } + } + } + } + trueDiscriminatorOwners.add(model.getClassname()); + } + } + } + + // Second pass: process child models + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel model = modelMap.getModel(); + String parentName = childToParentMap.get(model.getClassname()); + + if (parentName != null) { + CodegenModel parentModel = allModelsMap.get(parentName); + + if (model.getParent() == null) { + model.setParent(parentName); + } + + // If this child has a discriminator but it's inherited (not a true owner), + // remove it - only the parent should have the discriminator annotations + if (model.getDiscriminator() != null && !trueDiscriminatorOwners.contains(model.getClassname())) { + model.setDiscriminator(null); + } + + // For allOf pattern: if parent has properties, mark child's inherited properties + boolean parentIsOneOfOrAnyOf = parentModel != null + && ((parentModel.oneOf != null && !parentModel.oneOf.isEmpty()) + || (parentModel.anyOf != null && !parentModel.anyOf.isEmpty())); + + if (parentModel != null && parentModel.getHasVars() && !parentIsOneOfOrAnyOf) { + Set parentPropNames = new HashSet<>(); + List inheritedPropNamesList = new ArrayList<>(); + for (CodegenProperty parentProp : parentModel.getAllVars()) { + parentPropNames.add(parentProp.getBaseName()); + inheritedPropNamesList.add(parentProp.getName()); + } + + for (CodegenProperty prop : model.getAllVars()) { + if (parentPropNames.contains(prop.getBaseName())) { + prop.isInherited = true; + } + } + for (CodegenProperty prop : model.getVars()) { + if (parentPropNames.contains(prop.getBaseName())) { + prop.isInherited = true; + } + } + for (CodegenProperty prop : model.getRequiredVars()) { + if (parentPropNames.contains(prop.getBaseName())) { + prop.isInherited = true; + } + } + for (CodegenProperty prop : model.getOptionalVars()) { + if (parentPropNames.contains(prop.getBaseName())) { + prop.isInherited = true; + } + } + + if (!inheritedPropNamesList.isEmpty()) { + String parentCtorArgs = String.join(", ", inheritedPropNamesList.stream() + .map(name -> name + " = " + name) + .toArray(String[]::new)); + model.getVendorExtensions().put("x-parent-ctor-args", parentCtorArgs); + } + } + } + } + } + + // Third pass: set vendor extension for discriminator style and handle fixJacksonJsonTypeInfoInheritance + for (String ownerName : trueDiscriminatorOwners) { + CodegenModel owner = allModelsMap.get(ownerName); + if (owner != null && owner.getDiscriminator() != null) { + String discriminatorPropBaseName = owner.getDiscriminator().getPropertyBaseName(); + boolean isOneOfOrAnyOfPattern = (owner.oneOf != null && !owner.oneOf.isEmpty()) + || (owner.anyOf != null && !owner.anyOf.isEmpty()); + + boolean hasParentProperties = !isOneOfOrAnyOfPattern; + boolean visibleTrue; + + if (fixJacksonJsonTypeInfoInheritance) { + visibleTrue = true; + + if (isOneOfOrAnyOfPattern) { + String discriminatorVarName = toVarName(discriminatorPropBaseName); + + owner.getVars().clear(); + owner.getRequiredVars().clear(); + owner.getOptionalVars().clear(); + owner.getAllVars().clear(); + + CodegenProperty parentDiscriminatorProp = new CodegenProperty(); + parentDiscriminatorProp.baseName = discriminatorPropBaseName; + parentDiscriminatorProp.name = discriminatorVarName; + parentDiscriminatorProp.dataType = "kotlin.String"; + parentDiscriminatorProp.datatypeWithEnum = "kotlin.String"; + parentDiscriminatorProp.required = true; + parentDiscriminatorProp.isNullable = false; + parentDiscriminatorProp.isReadOnly = false; + + owner.getVars().add(parentDiscriminatorProp); + owner.getRequiredVars().add(parentDiscriminatorProp); + owner.getAllVars().add(parentDiscriminatorProp); + + hasParentProperties = true; + + for (CodegenDiscriminator.MappedModel mappedModel : owner.getDiscriminator().getMappedModels()) { + CodegenModel childModel = allModelsMap.get(mappedModel.getModelName()); + if (childModel != null) { + boolean hasDiscriminatorProp = false; + String discriminatorDefault = "\"" + mappedModel.getMappingName() + "\""; + + for (CodegenProperty prop : childModel.getVars()) { + if (prop.getBaseName().equals(discriminatorPropBaseName)) { + hasDiscriminatorProp = true; + prop.defaultValue = discriminatorDefault; + prop.dataType = "kotlin.String"; + prop.datatypeWithEnum = "kotlin.String"; + prop.required = true; + prop.isNullable = false; + prop.isInherited = true; + } + } + for (CodegenProperty prop : childModel.getAllVars()) { + if (prop.getBaseName().equals(discriminatorPropBaseName)) { + prop.defaultValue = discriminatorDefault; + prop.dataType = "kotlin.String"; + prop.datatypeWithEnum = "kotlin.String"; + prop.required = true; + prop.isNullable = false; + prop.isInherited = true; + } + } + + CodegenProperty propToMove = null; + for (CodegenProperty prop : childModel.getOptionalVars()) { + if (prop.getBaseName().equals(discriminatorPropBaseName)) { + prop.defaultValue = discriminatorDefault; + prop.dataType = "kotlin.String"; + prop.datatypeWithEnum = "kotlin.String"; + prop.required = true; + prop.isNullable = false; + prop.isInherited = true; + propToMove = prop; + break; + } + } + if (propToMove != null) { + childModel.getOptionalVars().remove(propToMove); + childModel.getRequiredVars().add(propToMove); + } + + for (CodegenProperty prop : childModel.getRequiredVars()) { + if (prop.getBaseName().equals(discriminatorPropBaseName)) { + prop.defaultValue = discriminatorDefault; + prop.dataType = "kotlin.String"; + prop.datatypeWithEnum = "kotlin.String"; + prop.isNullable = false; + prop.isInherited = true; + } + } + + if (!hasDiscriminatorProp) { + CodegenProperty discriminatorProp = new CodegenProperty(); + discriminatorProp.baseName = discriminatorPropBaseName; + discriminatorProp.name = discriminatorVarName; + discriminatorProp.dataType = "kotlin.String"; + discriminatorProp.datatypeWithEnum = "kotlin.String"; + discriminatorProp.defaultValue = discriminatorDefault; + discriminatorProp.required = true; + discriminatorProp.isNullable = false; + discriminatorProp.isReadOnly = false; + discriminatorProp.isInherited = true; + + childModel.getVars().add(discriminatorProp); + childModel.getRequiredVars().add(discriminatorProp); + childModel.getAllVars().add(discriminatorProp); + } + + // Update model flags after adding discriminator property + childModel.setHasVars(true); + childModel.setHasRequired(true); + + childModel.getVendorExtensions().put("x-parent-ctor-args", + discriminatorVarName + " = " + discriminatorVarName); + } + } + } + } else { + visibleTrue = hasParentProperties; + } + + owner.getVendorExtensions().put("x-discriminator-has-parent-properties", hasParentProperties); + owner.getDiscriminator().getVendorExtensions().put("x-discriminator-has-parent-properties", hasParentProperties); + owner.getVendorExtensions().put("x-discriminator-visible-true", visibleTrue); + owner.getDiscriminator().getVendorExtensions().put("x-discriminator-visible-true", visibleTrue); + } + } + + return objs; + } + @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache index 178487a26e3b..5e1655cfd7b7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache @@ -11,14 +11,23 @@ {{#vendorExtensions.x-class-extra-annotation}} {{{.}}} {{/vendorExtensions.x-class-extra-annotation}} -{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}( +{{#discriminator}}{{#useSealedDiscriminatorClasses}}{{! sealed class for discriminator models when opt-in flag is enabled +}}{{#vendorExtensions.x-discriminator-has-parent-properties}}sealed class {{classname}}( +{{#requiredVars}} +{{>dataClassSealedVar}}{{^-last}}, +{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, +{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassSealedVar}}{{^-last}}, +{{/-last}}{{/optionalVars}} +){{/vendorExtensions.x-discriminator-has-parent-properties}}{{! no newline +}}{{^vendorExtensions.x-discriminator-has-parent-properties}}sealed class {{classname}}{{/vendorExtensions.x-discriminator-has-parent-properties}}{{! no newline +}}{{/useSealedDiscriminatorClasses}}{{^useSealedDiscriminatorClasses}}interface {{classname}}{{/useSealedDiscriminatorClasses}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}( {{#requiredVars}} {{>dataClassReqVar}}{{^-last}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}}, {{/-last}}{{/optionalVars}} ){{/discriminator}}{{! no newline -}}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{! no newline +}}{{#parent}} : {{{.}}}{{#isMap}}(){{/isMap}}{{^isMap}}{{#useSealedDiscriminatorClasses}}({{#vendorExtensions.x-parent-ctor-args}}{{{vendorExtensions.x-parent-ctor-args}}}{{/vendorExtensions.x-parent-ctor-args}}){{/useSealedDiscriminatorClasses}}{{/isMap}}{{! no newline }}{{#vendorExtensions.x-kotlin-implements}}, {{{.}}}{{/vendorExtensions.x-kotlin-implements}}{{! <- serializableModel is also handled via x-kotlin-implements }}{{#vendorExtensions.x-implements-sealed-interfaces}}{{#.}}, {{{.}}}{{/.}}{{/vendorExtensions.x-implements-sealed-interfaces}}{{! <- add sealed interface implementations }}{{/parent}}{{! no newline @@ -33,12 +42,14 @@ }}{{/vendorExtensions.x-implements-sealed-interfaces}}{{! no newline }}{{/parent}} { {{#discriminator}} +{{^useSealedDiscriminatorClasses}} {{#requiredVars}} {{>interfaceReqVar}}{{! prevent indent}} {{/requiredVars}} {{#optionalVars}} {{>interfaceOptVar}}{{! prevent indent}} {{/optionalVars}} +{{/useSealedDiscriminatorClasses}} {{/discriminator}} {{#hasEnums}}{{#vars}}{{#isEnum}} /** diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassSealedVar.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassSealedVar.mustache new file mode 100644 index 000000000000..201d4dc91a78 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClassSealedVar.mustache @@ -0,0 +1,5 @@ +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} + @get:Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @get:ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#vendorExtensions.x-field-extra-annotation}} + {{{.}}}{{/vendorExtensions.x-field-extra-annotation}} + @get:JsonProperty("{{{baseName}}}") {{#isInherited}}override{{/isInherited}}{{^isInherited}}open{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache index b459faf88aa6..9b1ba001cab6 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache @@ -3,7 +3,7 @@ value = ["{{{discriminator.propertyBaseName}}}"], // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization ) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = {{#useSealedDiscriminatorClasses}}{{#vendorExtensions.x-discriminator-visible-true}}true{{/vendorExtensions.x-discriminator-visible-true}}{{^vendorExtensions.x-discriminator-visible-true}}false{{/vendorExtensions.x-discriminator-visible-true}}{{/useSealedDiscriminatorClasses}}{{^useSealedDiscriminatorClasses}}true{{/useSealedDiscriminatorClasses}}) @JsonSubTypes( {{#discriminator.mappedModels}} JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index dc250555135b..3211e5fcce8e 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -1520,8 +1520,6 @@ public void generateNonSerializableModelWithXimplements() throws Exception { path, "import java.io.Serializable", "Serializable", - ") : Pet, java.io.Serializable, com.some.pack.Fetchable {", - ") : Pet, java.io.Serializable {", "private const val serialVersionUID: kotlin.Long = 1" ); } @@ -4962,6 +4960,194 @@ public void shouldDefaultToJackson3WhenSpringBoot4EnabledViaSetter() throws IOEx assertFileNotContains(pomPath, "com.fasterxml.jackson.module"); assertFileNotContains(pomPath, "jackson-datatype-jsr310"); } + + // ==================== Polymorphism and Discriminator Tests ==================== + + @Test + public void oneOfWithDiscriminator_generatesSealedClassWithDiscriminatorProperty() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + + 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/model"; + Path petModel = Paths.get(outputPath + "/Pet.kt"); + + assertFileContains( + petModel, + "sealed class Pet(", + "open val petType: kotlin.String", + "@JsonTypeInfo", + "property = \"petType\"", + "visible = true", + "@JsonSubTypes" + ); + } + + @Test + public void oneOfWithDiscriminator_generatesChildrenWithOverrideDiscriminatorProperty() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + + 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/model"; + + Path catModel = Paths.get(outputPath + "/Cat.kt"); + assertFileContains( + catModel, + "data class Cat(", + "override val petType: kotlin.String = \"cat\"", + ") : Pet(petType = petType)" + ); + assertFileNotContains( + catModel, + "petType: kotlin.String?", + "petType: kotlin.Any" + ); + + Path dogModel = Paths.get(outputPath + "/Dog.kt"); + assertFileContains( + dogModel, + "data class Dog(", + "override val petType: kotlin.String = \"dog\"", + ") : Pet(petType = petType)" + ); + assertFileNotContains( + dogModel, + "petType: kotlin.String?", + "petType: kotlin.Any" + ); + } + + @Test + public void allOfWithDiscriminator_generatesSealedClassWithProperties() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/model"; + Path petModel = Paths.get(outputPath + "/Pet.kt"); + + assertFileContains( + petModel, + "sealed class Pet(", + "open val name: kotlin.String", + "open val petType: kotlin.String", + "@JsonTypeInfo", + "visible = true" + ); + } + + @Test + public void allOfWithDiscriminator_generatesChildrenWithOverrideProperties() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism-allof-and-discriminator.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/model"; + + Path catModel = Paths.get(outputPath + "/Cat.kt"); + assertFileContains( + catModel, + "data class Cat(", + "override val name: kotlin.String", + "override val petType: kotlin.String", + ") : Pet(name = name, petType = petType)" + ); + + Path dogModel = Paths.get(outputPath + "/Dog.kt"); + assertFileContains( + dogModel, + "data class Dog(", + "override val name: kotlin.String", + "override val petType: kotlin.String", + ") : Pet(name = name, petType = petType)" + ); + } + + @Test + public void polymorphismWithoutDiscriminator_generatesRegularDataClass() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/polymorphism.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/model"; + Path petModel = Paths.get(outputPath + "/Pet.kt"); + + assertFileContains( + petModel, + "data class Pet(" + ); + assertFileNotContains( + petModel, + "sealed class", + "@JsonTypeInfo", + "@JsonSubTypes" + ); + } + + @Test + public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SEALED_DISCRIMINATOR_CLASSES, true); + codegen.additionalProperties().put(FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, false); + + 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/model"; + Path petModel = Paths.get(outputPath + "/Pet.kt"); + + assertFileContains( + petModel, + "visible = false" + ); + } }