diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md
index 2bd68f57cb33..e73bc4584559 100644
--- a/docs/generators/java-camel.md
+++ b/docs/generators/java-camel.md
@@ -109,7 +109,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|title|server title name or client service name| |OpenAPI Spring|
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|
|useBeanValidation|Use BeanValidation API annotations| |true|
-|useDeductionForOneOfInterfaces|whether to use deduction for generated oneOf interfaces| |false|
+|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|
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md
index 1d0e83db8717..4ad436e2f39b 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. Only applies when library=spring-boot or spring-declarative-http-interface.| |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/docs/generators/spring.md b/docs/generators/spring.md
index 0e897c145fdc..9b2d4a2bc1eb 100644
--- a/docs/generators/spring.md
+++ b/docs/generators/spring.md
@@ -102,7 +102,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|title|server title name or client service name| |OpenAPI Spring|
|unhandledException|Declare operation methods to throw a generic exception and allow unhandled exceptions (useful for Spring `@ControllerAdvice` directives).| |false|
|useBeanValidation|Use BeanValidation API annotations| |true|
-|useDeductionForOneOfInterfaces|whether to use deduction for generated oneOf interfaces| |false|
+|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|
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
index 331ff7902b2b..ec9c342bb8aa 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
@@ -489,6 +489,13 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
public static final String X_MODEL_IS_MUTABLE = "x-model-is-mutable";
public static final String X_IMPLEMENTS = "x-implements";
public static final String X_IS_ONE_OF_INTERFACE = "x-is-one-of-interface";
+ public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES = "useDeductionForOneOfInterfaces";
+ public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC =
+ "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.";
public static final String X_DISCRIMINATOR_VALUE = "x-discriminator-value";
public static final String X_ONE_OF_NAME = "x-one-of-name";
public static final String X_NULLABLE = "x-nullable";
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 6d2a86061991..37cea6d84ee6 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. Only applies when library=spring-boot or spring-declarative-http-interface.",
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 76e20d09be36..de38d26ad105 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
@@ -110,7 +110,6 @@ public class SpringCodegen extends AbstractJavaCodegen
public static final String USE_SEALED = "useSealed";
public static final String OPTIONAL_ACCEPT_NULLABLE = "optionalAcceptNullable";
public static final String USE_SPRING_BUILT_IN_VALIDATION = "useSpringBuiltInValidation";
- public static final String USE_DEDUCTION_FOR_ONE_OF_INTERFACES = "useDeductionForOneOfInterfaces";
public static final String SPRING_API_VERSION = "springApiVersion";
public static final String USE_JACKSON_3 = "useJackson3";
public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson";
@@ -338,7 +337,7 @@ public SpringCodegen() {
"Use `ofNullable` instead of just `of` to accept null values when using Optional.",
optionalAcceptNullable));
- cliOptions.add(CliOption.newBoolean(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "whether to use deduction for generated oneOf interfaces", useDeductionForOneOfInterfaces));
+ cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces));
cliOptions.add(CliOption.newString(SPRING_API_VERSION, "Value for 'version' attribute in @RequestMapping (for Spring 7 and above)."));
cliOptions.add(CliOption.newString(USE_HTTP_SERVICE_PROXY_FACTORY_INTERFACES_CONFIGURATOR,
"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() {
}
convertPropertyToBooleanAndWriteBack(OPTIONAL_ACCEPT_NULLABLE, this::setOptionalAcceptNullable);
convertPropertyToBooleanAndWriteBack(USE_SPRING_BUILT_IN_VALIDATION, this::setUseSpringBuiltInValidation);
- convertPropertyToBooleanAndWriteBack(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces);
+ 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 95c8024deccc..8fb661a4b59d 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
@@ -5983,6 +5983,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();
diff --git a/samples/client/petstore/csharp/generichost/latest/NullTypes/src/Org.OpenAPITools/Client/ClientUtils.cs b/samples/client/petstore/csharp/generichost/latest/NullTypes/src/Org.OpenAPITools/Client/ClientUtils.cs
index 822ed3d850a9..a31448ff316e 100644
--- a/samples/client/petstore/csharp/generichost/latest/NullTypes/src/Org.OpenAPITools/Client/ClientUtils.cs
+++ b/samples/client/petstore/csharp/generichost/latest/NullTypes/src/Org.OpenAPITools/Client/ClientUtils.cs
@@ -301,6 +301,35 @@ public static bool IsJsonMime(string mime)
throw new JsonException("The specified discriminator was not found.");
}
+ ///
+ /// Determines if the provided header is a content header
+ ///
+ /// The header to check
+ /// True if a content header; False otherwise
+ public static bool IsContentHeader(string header)
+ {
+ return ContentHeaders.Contains(header.ToLowerInvariant());
+ }
+
+ ///
+ /// The collection of content headers as per
+ /// https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent.headers
+ ///
+ private static readonly string[] ContentHeaders = new String[]
+ {
+ "allow",
+ "content-encoding",
+ "content-disposition",
+ "content-language",
+ "content-length",
+ "content-location",
+ "content-md5",
+ "content-range",
+ "content-type",
+ "expires",
+ "last-modified"
+ };
+
///
/// The base path of the API
///