Skip to content
1 change: 1 addition & 0 deletions docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading