[Bug][java-spring] Use Flux only for multipart-form-data file parameters with multiple file uploads (#21561)

* Use Flux only for multipart-form-data file parameters with multiple files

* Update samples

---------

Co-authored-by: Chris Gual <cgual@omnidian.com>
This commit is contained in:
Christopher Gual
2025-07-28 03:28:53 -07:00
committed by GitHub
parent d69714f197
commit a60d3d4f81
16 changed files with 81 additions and 21 deletions

View File

@@ -275,7 +275,7 @@ public interface {{classname}} {
}
// Override this method
{{#jdk8-default-interface}}default {{/jdk8-default-interface}} {{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}{{#isArray}}List<MultipartFile>{{/isArray}}{{^isArray}}MultipartFile{{/isArray}}{{/reactive}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}} {
{{#jdk8-default-interface}}default {{/jdk8-default-interface}} {{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}} {
{{/delegate-method}}
{{^isDelegate}}
{{>methodBody}}{{! prevent indent}}

View File

@@ -71,7 +71,7 @@ public interface {{classname}}Delegate {
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
{{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#isArray}}List<{{/isArray}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}{{#isFormParam}}MultipartFile{{/isFormParam}}{{^isFormParam}}{{>optionalDataType}}{{/isFormParam}}{{/reactive}}{{#isArray}}>{{/isArray}}{{/isFile}} {{paramName}}{{^-last}},
{{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}{{#isFormParam}}MultipartFile{{/isFormParam}}{{^isFormParam}}{{>optionalDataType}}{{/isFormParam}}{{#isArray}}>{{/isArray}}{{/reactive}}{{/isFile}} {{paramName}}{{^-last}},
{{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
{{/hasParams}}ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
{{>methodBody}}{{! prevent indent}}

View File

@@ -1 +1 @@
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}} {{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{{dataType}}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#isArray}}List<{{/isArray}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}} {{paramName}}{{/isFile}}{{/isFormParam}}
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}} {{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{{dataType}}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}}

View File

@@ -733,6 +733,66 @@ public class SpringCodegenTest {
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"statusArray\"", "required", "false"));
}
@Test
public void testReactiveMultipartBoot() throws IOException {
final SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary("spring-boot");
codegen.setDelegatePattern(true);
codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, "springfox");
codegen.additionalProperties().put(SpringCodegen.REACTIVE, "true");
final Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/form-multipart-binary-array.yaml");
// Check that the delegate handles the array
JavaFileAssert.assertThat(files.get("MultipartArrayApiDelegate.java"))
.assertMethod("multipartArray", "Flux<Part>", "ServerWebExchange")
.assertParameter("files").hasType("Flux<Part>");
// Check that the api handles the array
JavaFileAssert.assertThat(files.get("MultipartArrayApi.java"))
.assertMethod("multipartArray", "Flux<Part>", "ServerWebExchange")
.assertParameter("files").hasType("Flux<Part>")
.assertParameterAnnotations()
.containsWithNameAndAttributes("ApiParam", ImmutableMap.of("value", "\"Many files\""))
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"files\"", "required", "false"));
// UPDATE: the following test has been ignored due to https://github.com/OpenAPITools/openapi-generator/pull/11081/
// We will contact the contributor of the following test to see if the fix will break their use cases and
// how we can fix it accordingly.
//// Check that the delegate handles the single file
// final File multipartSingleApiDelegate = files.get("MultipartSingleApiDelegate.java");
// assertFileContains(multipartSingleApiDelegate.toPath(), "MultipartFile file");
// Check that the api handles the single file
JavaFileAssert.assertThat(files.get("MultipartSingleApi.java"))
.assertMethod("multipartSingle", "Part", "ServerWebExchange")
.assertParameter("file").hasType("Part")
.assertParameterAnnotations()
.containsWithNameAndAttributes("ApiParam", ImmutableMap.of("value", "\"One file\""))
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"file\"", "required", "false"));
// Check that api validates mixed multipart request
JavaFileAssert.assertThat(files.get("MultipartMixedApi.java"))
.assertMethod("multipartMixed", "MultipartMixedStatus", "Part", "MultipartMixedRequestMarker", "List<MultipartMixedStatus>", "ServerWebExchange")
.assertParameter("status").hasType("MultipartMixedStatus")
.assertParameterAnnotations()
.containsWithName("Valid")
.containsWithNameAndAttributes("ApiParam", ImmutableMap.of("value", "\"\""))
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"status\"", "required", "true"))
.toParameter().toMethod()
.assertParameter("file").hasType("Part")
.assertParameterAnnotations()
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"file\"", "required", "true"))
.toParameter().toMethod()
.assertParameter("marker").hasType("MultipartMixedRequestMarker")
.assertParameterAnnotations()
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"marker\"", "required", "false"))
.toParameter().toMethod()
.assertParameter("statusArray").hasType("List<MultipartMixedStatus>")
.assertParameterAnnotations()
.containsWithNameAndAttributes("RequestPart", ImmutableMap.of("value", "\"statusArray\"", "required", "false"));
}
@Test
public void testAdditionalProperties_issue1466() throws IOException {
final SpringCodegen codegen = new SpringCodegen();

View File

@@ -226,7 +226,7 @@ public interface FakeApi {
@RequestPart(value = "int64", required = false) Long int64,
@RequestPart(value = "float", required = false) Float _float,
@RequestPart(value = "string", required = false) String string,
@RequestPart(value = "binary", required = false) Flux<Part> binary,
@RequestPart(value = "binary", required = false) Part binary,
@RequestPart(value = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestPart(value = "dateTime", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime dateTime,
@RequestPart(value = "password", required = false) String password,

View File

@@ -210,7 +210,7 @@ public interface PetApi {
Mono<ModelApiResponse> uploadFile(
@PathVariable("petId") Long petId,
@RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@RequestPart(value = "file", required = false) Flux<Part> file
@RequestPart(value = "file", required = false) Part file
);
@@ -232,7 +232,7 @@ public interface PetApi {
)
Mono<ModelApiResponse> uploadFileWithRequiredFile(
@PathVariable("petId") Long petId,
@RequestPart(value = "requiredFile", required = true) Flux<Part> requiredFile,
@RequestPart(value = "requiredFile", required = true) Part requiredFile,
@RequestPart(value = "additionalMetadata", required = false) String additionalMetadata
);

View File

@@ -217,7 +217,7 @@ public interface FakeApi {
@RequestPart(value = "int64", required = false) Long int64,
@RequestPart(value = "float", required = false) Float _float,
@RequestPart(value = "string", required = false) String string,
@RequestPart(value = "binary", required = false) Flux<Part> binary,
@RequestPart(value = "binary", required = false) Part binary,
@RequestPart(value = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestPart(value = "dateTime", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime dateTime,
@RequestPart(value = "password", required = false) String password,

View File

@@ -201,7 +201,7 @@ public interface PetApi {
Mono<ResponseEntity<ModelApiResponse>> uploadFile(
@PathVariable("petId") Long petId,
@RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@RequestPart(value = "file", required = false) Flux<Part> file
@RequestPart(value = "file", required = false) Part file
);
@@ -222,7 +222,7 @@ public interface PetApi {
)
Mono<ResponseEntity<ModelApiResponse>> uploadFileWithRequiredFile(
@PathVariable("petId") Long petId,
@RequestPart(value = "requiredFile", required = true) Flux<Part> requiredFile,
@RequestPart(value = "requiredFile", required = true) Part requiredFile,
@RequestPart(value = "additionalMetadata", required = false) String additionalMetadata
);

View File

@@ -396,7 +396,7 @@ public interface FakeApi {
@ApiParam(value = "None") @Valid @RequestPart(value = "int64", required = false) Long int64,
@ApiParam(value = "None") @Valid @RequestPart(value = "float", required = false) Float _float,
@ApiParam(value = "None") @Valid @RequestPart(value = "string", required = false) String string,
@ApiParam(value = "None") @RequestPart(value = "binary", required = false) Flux<Part> binary,
@ApiParam(value = "None") @RequestPart(value = "binary", required = false) Part binary,
@ApiParam(value = "None") @Valid @RequestPart(value = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@ApiParam(value = "None") @Valid @RequestPart(value = "dateTime", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime dateTime,
@ApiParam(value = "None") @Valid @RequestPart(value = "password", required = false) String password,
@@ -698,7 +698,7 @@ public interface FakeApi {
default Mono<ModelApiResponse> uploadFileWithRequiredFile(
@ApiParam(value = "ID of pet to update", required = true) @PathVariable("petId") Long petId,
@ApiParam(value = "file to upload", required = true) @RequestPart(value = "requiredFile", required = true) Flux<Part> requiredFile,
@ApiParam(value = "file to upload", required = true) @RequestPart(value = "requiredFile", required = true) Part requiredFile,
@ApiParam(value = "Additional data to pass to server") @Valid @RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@ApiIgnore final ServerWebExchange exchange
) {

View File

@@ -238,7 +238,7 @@ public interface FakeApiDelegate {
Long int64,
Float _float,
String string,
Flux<Part> binary,
Part binary,
LocalDate date,
OffsetDateTime dateTime,
String password,
@@ -411,7 +411,7 @@ public interface FakeApiDelegate {
* @see FakeApi#uploadFileWithRequiredFile
*/
default Mono<ModelApiResponse> uploadFileWithRequiredFile(Long petId,
Flux<Part> requiredFile,
Part requiredFile,
String additionalMetadata,
ServerWebExchange exchange) {
Mono<Void> result = Mono.empty();

View File

@@ -365,7 +365,7 @@ public interface PetApi {
default Mono<ModelApiResponse> uploadFile(
@ApiParam(value = "ID of pet to update", required = true) @PathVariable("petId") Long petId,
@ApiParam(value = "Additional data to pass to server") @Valid @RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@ApiParam(value = "file to upload") @RequestPart(value = "file", required = false) Flux<Part> file,
@ApiParam(value = "file to upload") @RequestPart(value = "file", required = false) Part file,
@ApiIgnore final ServerWebExchange exchange
) {
return getDelegate().uploadFile(petId, additionalMetadata, file, exchange);

View File

@@ -209,7 +209,7 @@ public interface PetApiDelegate {
*/
default Mono<ModelApiResponse> uploadFile(Long petId,
String additionalMetadata,
Flux<Part> file,
Part file,
ServerWebExchange exchange) {
Mono<Void> result = Mono.empty();
exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);

View File

@@ -386,7 +386,7 @@ public interface FakeApi {
@ApiParam(value = "None") @Valid @RequestPart(value = "int64", required = false) Long int64,
@ApiParam(value = "None") @Valid @RequestPart(value = "float", required = false) Float _float,
@ApiParam(value = "None") @Valid @RequestPart(value = "string", required = false) String string,
@ApiParam(value = "None") @RequestPart(value = "binary", required = false) Flux<Part> binary,
@ApiParam(value = "None") @RequestPart(value = "binary", required = false) Part binary,
@ApiParam(value = "None") @Valid @RequestPart(value = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@ApiParam(value = "None") @Valid @RequestPart(value = "dateTime", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime dateTime,
@ApiParam(value = "None") @Valid @RequestPart(value = "password", required = false) String password,
@@ -680,7 +680,7 @@ public interface FakeApi {
default Mono<ResponseEntity<ModelApiResponse>> uploadFileWithRequiredFile(
@ApiParam(value = "ID of pet to update", required = true) @PathVariable("petId") Long petId,
@ApiParam(value = "file to upload", required = true) @RequestPart(value = "requiredFile", required = true) Flux<Part> requiredFile,
@ApiParam(value = "file to upload", required = true) @RequestPart(value = "requiredFile", required = true) Part requiredFile,
@ApiParam(value = "Additional data to pass to server") @Valid @RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@ApiIgnore final ServerWebExchange exchange
) {

View File

@@ -239,7 +239,7 @@ public interface FakeApiDelegate {
Long int64,
Float _float,
String string,
Flux<Part> binary,
Part binary,
LocalDate date,
OffsetDateTime dateTime,
String password,
@@ -412,7 +412,7 @@ public interface FakeApiDelegate {
* @see FakeApi#uploadFileWithRequiredFile
*/
default Mono<ResponseEntity<ModelApiResponse>> uploadFileWithRequiredFile(Long petId,
Flux<Part> requiredFile,
Part requiredFile,
String additionalMetadata,
ServerWebExchange exchange) {
Mono<Void> result = Mono.empty();

View File

@@ -357,7 +357,7 @@ public interface PetApi {
default Mono<ResponseEntity<ModelApiResponse>> uploadFile(
@ApiParam(value = "ID of pet to update", required = true) @PathVariable("petId") Long petId,
@ApiParam(value = "Additional data to pass to server") @Valid @RequestPart(value = "additionalMetadata", required = false) String additionalMetadata,
@ApiParam(value = "file to upload") @RequestPart(value = "file", required = false) Flux<Part> file,
@ApiParam(value = "file to upload") @RequestPart(value = "file", required = false) Part file,
@ApiIgnore final ServerWebExchange exchange
) {
return getDelegate().uploadFile(petId, additionalMetadata, file, exchange);

View File

@@ -210,7 +210,7 @@ public interface PetApiDelegate {
*/
default Mono<ResponseEntity<ModelApiResponse>> uploadFile(Long petId,
String additionalMetadata,
Flux<Part> file,
Part file,
ServerWebExchange exchange) {
Mono<Void> result = Mono.empty();
exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);