diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 42c864eed0f8..2ac49f9d6eaf 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/java-microprofile.md b/docs/generators/java-microprofile.md index 240e822a7f8c..1f7f33c0b8b1 100644 --- a/docs/generators/java-microprofile.md +++ b/docs/generators/java-microprofile.md @@ -95,6 +95,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |useAbstractionForFiles|Use alternative types instead of java.io.File to allow passing bytes without a file on disk. Available on resttemplate, webclient, restclient, libraries| |false| |useBeanValidation|Use BeanValidation API annotations| |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| |useGzipFeature|Send gzip-encoded requests| |false| |useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' and 'apache-httpclient' libraries (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true).| |false| diff --git a/docs/generators/java.md b/docs/generators/java.md index b1046f765a78..f8a8c5a74019 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -95,6 +95,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |useAbstractionForFiles|Use alternative types instead of java.io.File to allow passing bytes without a file on disk. Available on resttemplate, webclient, restclient, libraries| |false| |useBeanValidation|Use BeanValidation API annotations| |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| |useGzipFeature|Send gzip-encoded requests| |false| |useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' and 'apache-httpclient' libraries (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true).| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index e1a4d7771e61..0542bced8084 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..7dd1947471c1 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/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 68aa5e7b6196..6cf3ca55fee2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -75,6 +75,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES; import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS; import static org.openapitools.codegen.utils.CamelizeOption.*; import static org.openapitools.codegen.utils.ModelUtils.getSchemaItems; @@ -225,6 +226,8 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original} @Setter protected boolean useJspecify; protected JSpecifyNullableLambda jSpecifyNullableLambda; + @Getter @Setter + protected boolean useDeductionForOneOfInterfaces = false; private Map schemaKeyToModelNameCache = new HashMap<>(); @@ -608,6 +611,7 @@ public void processOpts() { convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces); convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming); convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify); + convertPropertyToBooleanAndWriteBack(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces); if (!StringUtils.isEmpty(parentGroupId) && !StringUtils.isEmpty(parentArtifactId) && !StringUtils.isEmpty(parentVersion)) { additionalProperties.put("parentOverridden", true); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 26f1b6bd3abe..1cad2361a810 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -48,7 +48,7 @@ import static com.google.common.base.CaseFormat.LOWER_CAMEL; import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; import static java.util.Collections.sort; -import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS; +import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.camelize; @@ -285,6 +285,7 @@ public JavaClientCodegen() { cliOptions.add(CliOption.newBoolean(USE_SEALED_ONE_OF_INTERFACES, "Generate the oneOf interfaces as sealed interfaces. Only supported for WebClient and RestClient.", this.useSealedOneOfInterfaces)); cliOptions.add(CliOption.newBoolean(USE_UNARY_INTERCEPTOR, "If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption", this.useUnaryInterceptor)); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks. Only supported for " + JSPECIFY_SUPPORTED_LIBRARIES, useJspecify)); + cliOptions.add(CliOption.newBoolean(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces)); supportedLibraries.put(JERSEY2, "HTTP client: Jersey client 2.25.1. JSON processing: Jackson 2.17.1"); supportedLibraries.put(JERSEY3, "HTTP client: Jersey client 3.1.1. JSON processing: Jackson 2.17.1"); 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 9329e8a1b5c5..695b9451574c 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 @@ -54,6 +54,8 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES; +import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.camelize; @@ -110,7 +112,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"; @@ -187,8 +188,6 @@ public enum RequestMappingMode { @Getter @Setter protected boolean useSpringBuiltInValidation = false; @Getter @Setter - protected boolean useDeductionForOneOfInterfaces = false; - @Getter @Setter protected boolean useJackson3 = false; @Getter @Setter protected boolean additionalNotNullAnnotations = false; @@ -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(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, 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,6 @@ 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); additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda()); diff --git a/modules/openapi-generator/src/main/resources/Java/deductionAnnotation.mustache b/modules/openapi-generator/src/main/resources/Java/deductionAnnotation.mustache new file mode 100644 index 000000000000..c59bc4cbfd17 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/Java/deductionAnnotation.mustache @@ -0,0 +1,12 @@ +{{^discriminator}} +{{#jackson}} +{{#useDeductionForOneOfInterfaces}} + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + @JsonSubTypes({ + {{#interfaceModels}} + @JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}} + {{/interfaceModels}} + }) +{{/useDeductionForOneOfInterfaces}} +{{/jackson}} +{{/discriminator}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache index 7a77b67ae169..742de98954c7 100644 --- a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache @@ -1,4 +1,4 @@ -{{>additionalOneOfTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} +{{>additionalOneOfTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>deductionAnnotation}}{{>xmlAnnotation}} {{#vendorExtensions.x-class-extra-annotation}} {{{vendorExtensions.x-class-extra-annotation}}} {{/vendorExtensions.x-class-extra-annotation}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 69fb7de07ada..298f4c8b059f 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -4452,4 +4452,18 @@ void oneOf_issue_912() { .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Source.class", "name", "\"source\"")); } + @Test + public void testUseDeductionForOneInterfaces() { + final Map files = generateFromContract("src/test/resources/3_1/oneof_polymorphism_and_inheritance.yaml", RESTCLIENT, + Map.of(USE_ONE_OF_INTERFACES, "true", USE_DEDUCTION_FOR_ONE_OF_INTERFACES, "true")); + JavaFileAssert.assertThat(files.get("Animal.java")).fileContains("@JsonSubTypes") + .isInterface() + .assertTypeAnnotations().containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Dog.class")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Cat.class")) + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("use", "JsonTypeInfo.Id.DEDUCTION")); + + } + + }