Skip to content

Commit ad908f5

Browse files
committed
feat(kotlin-spring): add support for deduction in oneOf interfaces with Jackson annotations
1 parent 5589824 commit ad908f5

8 files changed

Lines changed: 59 additions & 5 deletions

File tree

docs/generators/java-camel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
109109
|title|server title name or client service name| |OpenAPI Spring|
110110
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|
111111
|useBeanValidation|Use BeanValidation API annotations| |true|
112-
|useDeductionForOneOfInterfaces|whether to use deduction for generated oneOf interfaces| |false|
112+
|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|
113113
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
114114
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
115115
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|

docs/generators/kotlin-spring.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
6161
|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. Only applies when library=spring-boot or spring-declarative-http-interface.| |false|
6262
|title|server title name or client service name| |OpenAPI Kotlin Spring|
6363
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
64+
|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|
6465
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
6566
|useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true|
6667
|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled. Incompatible with `openApiNullable`.| |false|

docs/generators/spring.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
102102
|title|server title name or client service name| |OpenAPI Spring|
103103
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|
104104
|useBeanValidation|Use BeanValidation API annotations| |true|
105-
|useDeductionForOneOfInterfaces|whether to use deduction for generated oneOf interfaces| |false|
105+
|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|
106106
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
107107
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
108108
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,13 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
489489
public static final String X_MODEL_IS_MUTABLE = "x-model-is-mutable";
490490
public static final String X_IMPLEMENTS = "x-implements";
491491
public static final String X_IS_ONE_OF_INTERFACE = "x-is-one-of-interface";
492+
public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES = "useDeductionForOneOfInterfaces";
493+
public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC =
494+
"Annotate discriminator-free oneOf interfaces with Jackson's " +
495+
"@JsonTypeInfo(use = Id.DEDUCTION) and @JsonSubTypes so the concrete subtype " +
496+
"is resolved from the JSON field set rather than a type-tag property. " +
497+
"Has no effect when a discriminator is present (name-based resolution is used instead). " +
498+
"Requires subtypes to have structurally distinct sets of properties.";
492499
public static final String X_DISCRIMINATOR_VALUE = "x-discriminator-value";
493500
public static final String X_ONE_OF_NAME = "x-one-of-name";
494501
public static final String X_NULLABLE = "x-nullable";

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ public String getDescription() {
181181
@Setter private boolean substituteGenericPagedModel = false;
182182
@Setter private boolean useSealedResponseInterfaces = false;
183183
@Setter private boolean companionObject = false;
184+
@Getter @Setter
185+
protected boolean useDeductionForOneOfInterfaces = false;
184186

185187
@Getter @Setter
186188
protected boolean useSpringBoot3 = false;
@@ -311,6 +313,7 @@ public KotlinSpringServerCodegen() {
311313
+ "schema are suppressed from code generation. Only applies when library=spring-boot or spring-declarative-http-interface.",
312314
substituteGenericPagedModel);
313315
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
316+
cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces));
314317
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
315318
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
316319
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
@@ -572,6 +575,8 @@ public void processOpts() {
572575
additionalProperties.put(COMPANION_OBJECT, companionObject);
573576
}
574577

578+
convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces);
579+
575580
additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda());
576581

577582
// Set basePackage from invokerPackage

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ public class SpringCodegen extends AbstractJavaCodegen
110110
public static final String USE_SEALED = "useSealed";
111111
public static final String OPTIONAL_ACCEPT_NULLABLE = "optionalAcceptNullable";
112112
public static final String USE_SPRING_BUILT_IN_VALIDATION = "useSpringBuiltInValidation";
113-
public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES = "useDeductionForOneOfInterfaces";
114113
public static final String SPRING_API_VERSION = "springApiVersion";
115114
public static final String USE_JACKSON_3 = "useJackson3";
116115
public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson";
@@ -338,7 +337,7 @@ public SpringCodegen() {
338337
"Use `ofNullable` instead of just `of` to accept null values when using Optional.",
339338
optionalAcceptNullable));
340339

341-
cliOptions.add(CliOption.newBoolean(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "whether to use deduction for generated oneOf interfaces", useDeductionForOneOfInterfaces));
340+
cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces));
342341
cliOptions.add(CliOption.newString(SPRING_API_VERSION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above)."));
343342
cliOptions.add(CliOption.newString(USE_HTTP_SERVICE_PROXY_FACTORY_INTERFACES_CONFIGURATOR,
344343
"Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.")
@@ -557,7 +556,7 @@ public void processOpts() {
557556
}
558557
convertPropertyToBooleanAndWriteBack(OPTIONAL_ACCEPT_NULLABLE, this::setOptionalAcceptNullable);
559558
convertPropertyToBooleanAndWriteBack(USE_SPRING_BUILT_IN_VALIDATION, this::setUseSpringBuiltInValidation);
560-
convertPropertyToBooleanAndWriteBack(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces);
559+
convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces);
561560

562561
additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda());
563562

modules/openapi-generator/src/main/resources/kotlin-spring/oneof_interface.mustache

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
{{#discriminator}}
55
{{>typeInfoAnnotation}}
66
{{/discriminator}}
7+
{{^discriminator}}{{#useDeductionForOneOfInterfaces}}{{#jackson}}
8+
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
9+
@JsonSubTypes(
10+
{{#interfaceModels}}
11+
JsonSubTypes.Type(value = {{classname}}::class){{^-last}},{{/-last}}
12+
{{/interfaceModels}}
13+
)
14+
{{/jackson}}{{/useDeductionForOneOfInterfaces}}{{/discriminator}}
715
{{#additionalModelTypeAnnotations}}
816
{{{.}}}
917
{{/additionalModelTypeAnnotations}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5983,6 +5983,40 @@ public void testOneOfRefEnumDiscriminatorResolvesType() throws IOException {
59835983
);
59845984
}
59855985

5986+
@Test(description = "oneOf without discriminator with useDeductionForOneOfInterfaces generates @JsonTypeInfo(DEDUCTION) annotation")
5987+
public void testOneOfDeductionWithoutDiscriminatorGeneratesDeductionAnnotation() throws IOException {
5988+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
5989+
output.deleteOnExit();
5990+
5991+
new DefaultGenerator().opts(new ClientOptInput()
5992+
.openAPI(new OpenAPIParser().readLocation("src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml", null, new ParseOptions()).getOpenAPI())
5993+
.config(new KotlinSpringServerCodegen() {{
5994+
setOutputDir(output.getAbsolutePath());
5995+
additionalProperties().put(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "true");
5996+
}}))
5997+
.generate();
5998+
5999+
String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/model";
6000+
6001+
// Animal has oneOf [Dog, Cat] with NO discriminator → deduction should be applied
6002+
assertFileContains(Paths.get(outputPath + "/Animal.kt"),
6003+
"sealed interface Animal",
6004+
"@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)",
6005+
"@JsonSubTypes(",
6006+
"JsonSubTypes.Type(value = Dog::class)",
6007+
"JsonSubTypes.Type(value = Cat::class)"
6008+
);
6009+
6010+
// Fruit has oneOf [Apple, Banana] WITH a discriminator → must NOT use deduction
6011+
assertFileNotContains(Paths.get(outputPath + "/Fruit.kt"),
6012+
"JsonTypeInfo.Id.DEDUCTION"
6013+
);
6014+
assertFileContains(Paths.get(outputPath + "/Fruit.kt"),
6015+
"sealed interface Fruit",
6016+
"@JsonTypeInfo(use = JsonTypeInfo.Id.NAME"
6017+
);
6018+
}
6019+
59866020
@Test
59876021
public void testSealedResponseInterfacesWithDeclarativeHttpInterface() throws IOException {
59886022
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();

0 commit comments

Comments
 (0)