diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index e8f7eb86e02a..92f3b3d093a1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -1052,6 +1052,8 @@ public boolean specVersionGreaterThanOrEqualTo310(OpenAPI openAPI) { return specMajorVersion == 3 && specMinorVersion >= 1; } + private List schemasUsedOnlyInFormParam = null; + /** * Set the OpenAPI document. * This method is invoked when the input OpenAPI document has been parsed and validated. @@ -1062,6 +1064,7 @@ public void setOpenAPI(OpenAPI openAPI) { once(LOGGER).warn(UNSUPPORTED_V310_SPEC_MSG); } this.openAPI = openAPI; + this.schemasUsedOnlyInFormParam = null; // Set global settings such that helper functions in ModelUtils can lookup the value // of the CLI option. ModelUtils.setDisallowAdditionalPropertiesIfNotPresent(getDisallowAdditionalPropertiesIfNotPresent()); @@ -1070,6 +1073,21 @@ public void setOpenAPI(OpenAPI openAPI) { typeAliases = getAllAliases(ModelUtils.getSchemas(openAPI)); } + /** + * Returns the list of schema names that are used only in form parameters. + * + * @return list of schema names used only in form parameters + */ + public List getSchemasUsedOnlyInFormParam() { + if (this.openAPI == null) { + return Collections.emptyList(); + } + if (this.schemasUsedOnlyInFormParam == null) { + this.schemasUsedOnlyInFormParam = ModelUtils.getSchemasUsedOnlyInFormParam(this.openAPI); + } + return this.schemasUsedOnlyInFormParam; + } + // override with any message to be shown right before the process finishes @Override @SuppressWarnings("static-method") @@ -8214,6 +8232,36 @@ private CodegenParameter headerToCodegenParameter(Header header, String headerNa return param; } + /** + * Check if the content type is form data (e.g. multipart/* or application/x-www-form-urlencoded). + * + * @param contentType Content type string + * @return true if form content type + */ + protected boolean isFormContentType(String contentType) { + if (contentType == null) { + return false; + } + String ct = contentType.toLowerCase(Locale.ROOT); + return ct.startsWith("application/x-www-form-urlencoded") || ct.startsWith("multipart"); + } + + /** + * Check if skipFormModel is enabled (defaults to true). + * + * @return true if skipFormModel is true + */ + public boolean isSkipFormModel() { + if (additionalProperties.containsKey(CodegenConstants.SKIP_FORM_MODEL)) { + final Object val = additionalProperties.get(CodegenConstants.SKIP_FORM_MODEL); + if (val instanceof Boolean) { + return (Boolean) val; + } + return Boolean.parseBoolean(val.toString()); + } + return Boolean.parseBoolean(GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL, "true")); + } + protected LinkedHashMap getContent(Content content, Set imports, String mediaTypeSchemaSuffix) { if (content == null) { return null; @@ -8286,7 +8334,17 @@ protected LinkedHashMap getContent(Content content, Se cmtContent.put(contentType, codegenMt); if (schemaProp != null) { - addImports(imports, schemaProp.getImports(true, importBaseType, generatorMetadata.getFeatureSet())); + Set propImports = schemaProp.getImports(true, importBaseType, generatorMetadata.getFeatureSet()); + if (isFormContentType(contentType) && isSkipFormModel()) { + List formOnlySchemas = getSchemasUsedOnlyInFormParam(); + Set formOnlyModels = formOnlySchemas.stream() + .flatMap(s -> Stream.of(s, toModelName(s))) + .collect(Collectors.toSet()); + propImports = propImports.stream() + .filter(imp -> !formOnlyModels.contains(imp)) + .collect(Collectors.toSet()); + } + addImports(imports, propImports); } } return cmtContent; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 6636187fec0a..309dead212ca 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -192,6 +192,14 @@ private Boolean getGeneratorPropertyDefaultSwitch(final String key, final Boolea return defaultValue; } + private Boolean getGlobalOrGeneratorPropertyDefault(final String key, final Boolean defaultValue) { + String global = GlobalSettings.getProperty(key); + if (global != null) { + return Boolean.valueOf(global); + } + return getGeneratorPropertyDefaultSwitch(key, defaultValue); + } + void configureGeneratorProperties() { // allows generating only models by specifying a CSV of models to generate, or empty for all // NOTE: Boolean.TRUE is required below rather than `true` because of JVM boxing constraints and type inference. @@ -219,11 +227,12 @@ void configureGeneratorProperties() { } // model/api tests and documentation options rely on parent generate options (api or model) and no other options. // They default to true in all scenarios and can only be marked false explicitly - generateModelTests = GlobalSettings.getProperty(CodegenConstants.MODEL_TESTS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.MODEL_TESTS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODEL_TESTS, true); - generateModelDocumentation = GlobalSettings.getProperty(CodegenConstants.MODEL_DOCS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.MODEL_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODEL_DOCS, true); - generateApiTests = GlobalSettings.getProperty(CodegenConstants.API_TESTS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.API_TESTS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_TESTS, true); - generateApiDocumentation = GlobalSettings.getProperty(CodegenConstants.API_DOCS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.API_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_DOCS, true); - generateRecursiveDependentModels = GlobalSettings.getProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, false); + generateModelTests = getGlobalOrGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, true); + generateModelDocumentation = getGlobalOrGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, true); + generateApiTests = getGlobalOrGeneratorPropertyDefault(CodegenConstants.API_TESTS, true); + generateApiDocumentation = getGlobalOrGeneratorPropertyDefault(CodegenConstants.API_DOCS, true); + generateRecursiveDependentModels = getGlobalOrGeneratorPropertyDefault(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, false); + Boolean skipFormModel = getGlobalOrGeneratorPropertyDefault(CodegenConstants.SKIP_FORM_MODEL, true); // Additional properties added for tests to exclude references in project related files config.additionalProperties().put(CodegenConstants.GENERATE_API_TESTS, generateApiTests); @@ -236,6 +245,7 @@ void configureGeneratorProperties() { config.additionalProperties().put(CodegenConstants.GENERATE_MODELS, generateModels); config.additionalProperties().put(CodegenConstants.GENERATE_WEBHOOKS, generateWebhooks); config.additionalProperties().put(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, generateRecursiveDependentModels); + config.additionalProperties().put(CodegenConstants.SKIP_FORM_MODEL, skipFormModel); if (!generateApiTests && !generateModelTests) { config.additionalProperties().put(CodegenConstants.EXCLUDE_TESTS, true); @@ -470,9 +480,9 @@ void generateModels(List files, List allModels, List unu // store all processed models Map allProcessedModels = new TreeMap<>((o1, o2) -> ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2))); - Boolean skipFormModel = GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL) != null ? - Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL)) : - getGeneratorPropertyDefaultSwitch(CodegenConstants.SKIP_FORM_MODEL, true); + Boolean skipFormModel = config.additionalProperties().containsKey(CodegenConstants.SKIP_FORM_MODEL) ? + Boolean.valueOf(config.additionalProperties().get(CodegenConstants.SKIP_FORM_MODEL).toString()) : + Boolean.TRUE; // process models only for (String name : modelKeys) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index b7184f853396..ca82fc1f1316 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -9335,4 +9335,70 @@ public void issue_24232() throws IOException { JavaFileAssert.assertThat(files.get("Dummy.java")) .fileContains("import org.myorg.MyCustomId;", "import org.myorg.MyCustomKey;"); } + + @Test + public void testMultipleRequestBodyContentTypesDanglingImport_issue24727() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(USE_TAGS, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/issue_24727.yaml", SPRING_BOOT, + additionalProperties); + + JavaFileAssert.assertThat(files.get("DefaultApi.java")) + .fileContains("import org.openapitools.model.MultiArticleImporter;") + .fileDoesNotContain("import org.openapitools.model.CreateMultiArticleImporterRequest;"); + + Assert.assertNotNull(files.get("MultiArticleImporter.java")); + Assert.assertNull(files.get("CreateMultiArticleImporterRequest.java")); + } + + @Test + public void testMultipleRequestBodyContentTypesWithSkipFormModelFalse_issue24727() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/issue_24727.yaml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + codegen.additionalProperties().put(USE_TAGS, "true"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGeneratorPropertyDefault(CodegenConstants.SKIP_FORM_MODEL, "false"); + generator.setGenerateMetadata(false); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(this::getUniqueName, Function.identity())); + + JavaFileAssert.assertThat(files.get("DefaultApi.java")) + .fileContains("import org.openapitools.model.MultiArticleImporter;") + .fileContains("import org.openapitools.model.CreateMultiArticleImporterRequest;"); + + Assert.assertNotNull(files.get("MultiArticleImporter.java")); + Assert.assertNotNull(files.get("CreateMultiArticleImporterRequest.java")); + } + + @Test + public void testMultipleRequestBodyContentTypesWithSharedModel_issue24727() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(INTERFACE_ONLY, "true"); + additionalProperties.put(USE_TAGS, "true"); + + Map files = generateFromContract( + "src/test/resources/3_0/issue_24727_shared.yaml", SPRING_BOOT, + additionalProperties); + + JavaFileAssert.assertThat(files.get("DefaultApi.java")) + .fileContains("import org.openapitools.model.ArticlePayload;"); + + Assert.assertNotNull(files.get("ArticlePayload.java")); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_24727.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_24727.yaml new file mode 100644 index 000000000000..d2c5ef337e2b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_24727.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.3 +info: + title: Multiple request-body content types repro + version: 1.0.0 +paths: + /api/multi_article_importer: + post: + operationId: createMultiArticleImporter + summary: Create a multi article importer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MultiArticleImporter' + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/MultiArticleImporter' +components: + schemas: + MultiArticleImporter: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_24727_shared.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_24727_shared.yaml new file mode 100644 index 000000000000..0d8686b1b7cf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_24727_shared.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.3 +info: + title: Shared form model repro + version: 1.0.0 +paths: + /api/upload: + post: + operationId: uploadArticle + summary: Upload an article + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/ArticlePayload' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ArticlePayload' +components: + schemas: + ArticlePayload: + type: object + properties: + id: + type: integer + format: int64 + title: + type: string