diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index b9ede8f82135..584c4e1c9cb3 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -61,6 +61,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false| |title|server title name or client service name| |OpenAPI Kotlin Spring| |useBeanValidation|Use BeanValidation API annotations to validate data types| |true| +|useDeductionForOneOfInterfaces|Annotate discriminator-free oneOf interfaces with Jackson's @JsonTypeInfo(use = Id.DEDUCTION) and @JsonSubTypes so the concrete subtype is resolved from the JSON field set rather than a type-tag property. Has no effect when a discriminator is present (name-based resolution is used instead). Requires subtypes to have structurally distinct sets of properties.| |false| |useFeignClientUrl|Whether to generate Feign client with url parameter.| |true| |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| 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 1fb3c78a9e89..e3315a25cea7 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 @@ -181,6 +181,8 @@ public String getDescription() { @Setter private boolean substituteGenericPagedModel = false; @Setter private boolean useSealedResponseInterfaces = false; @Setter private boolean companionObject = false; + @Getter @Setter + protected boolean useDeductionForOneOfInterfaces = false; @Getter @Setter protected boolean useSpringBoot3 = false; @@ -311,6 +313,7 @@ public KotlinSpringServerCodegen() { + "schema are suppressed from code generation.", substituteGenericPagedModel); addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); + cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces)); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, "Spring-Cloud-Feign client with Spring-Boot auto-configured settings."); @@ -572,6 +575,8 @@ public void processOpts() { additionalProperties.put(COMPANION_OBJECT, companionObject); } + convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces); + additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda()); // Set basePackage from invokerPackage diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 695b9451574c..d0b94b240f30 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -556,6 +556,7 @@ public void processOpts() { } convertPropertyToBooleanAndWriteBack(OPTIONAL_ACCEPT_NULLABLE, this::setOptionalAcceptNullable); convertPropertyToBooleanAndWriteBack(USE_SPRING_BUILT_IN_VALIDATION, this::setUseSpringBuiltInValidation); + convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces); additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda()); diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache index de545671d8b3..06e5c7943fe7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache @@ -4,7 +4,14 @@ {{#discriminator}} {{>typeInfoAnnotation}} {{/discriminator}} -{{#additionalModelTypeAnnotations}} +{{^discriminator}}{{#useDeductionForOneOfInterfaces}}{{#jackson}} +@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) +@JsonSubTypes( + {{#interfaceModels}} + JsonSubTypes.Type(value = {{classname}}::class){{^-last}},{{/-last}} + {{/interfaceModels}} +) +{{/jackson}}{{/useDeductionForOneOfInterfaces}}{{/discriminator}}{{#additionalModelTypeAnnotations}} {{{.}}} {{/additionalModelTypeAnnotations}} {{#vendorExtensions.x-class-extra-annotation}} 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 9f17ad216091..586920585f7f 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 @@ -6050,6 +6050,40 @@ public void testOneOfRefEnumDiscriminatorResolvesType() throws IOException { ); } + @Test(description = "oneOf without discriminator with useDeductionForOneOfInterfaces generates @JsonTypeInfo(DEDUCTION) annotation") + public void testOneOfDeductionWithoutDiscriminatorGeneratesDeductionAnnotation() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(new OpenAPIParser().readLocation("src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml", null, new ParseOptions()).getOpenAPI()) + .config(new KotlinSpringServerCodegen() {{ + setOutputDir(output.getAbsolutePath()); + additionalProperties().put(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "true"); + }})) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/model"; + + // Animal has oneOf [Dog, Cat] with NO discriminator → deduction should be applied + assertFileContains(Paths.get(outputPath + "/Animal.kt"), + "sealed interface Animal", + "@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)", + "@JsonSubTypes(", + "JsonSubTypes.Type(value = Dog::class)", + "JsonSubTypes.Type(value = Cat::class)" + ); + + // Fruit has oneOf [Apple, Banana] WITH a discriminator → must NOT use deduction + assertFileNotContains(Paths.get(outputPath + "/Fruit.kt"), + "JsonTypeInfo.Id.DEDUCTION" + ); + assertFileContains(Paths.get(outputPath + "/Fruit.kt"), + "sealed interface Fruit", + "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME" + ); + } + @Test public void testSealedResponseInterfacesWithDeclarativeHttpInterface() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile();