diff --git a/bin/configs/typescript-fetch-multipart-file-array.yaml b/bin/configs/typescript-fetch-multipart-file-array.yaml
new file mode 100644
index 000000000000..26aa67eb790f
--- /dev/null
+++ b/bin/configs/typescript-fetch-multipart-file-array.yaml
@@ -0,0 +1,4 @@
+generatorName: typescript-fetch
+outputDir: samples/client/others/typescript-fetch/multipart-file-array
+inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml
+templateDir: modules/openapi-generator/src/main/resources/typescript-fetch
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java
index c5206ec9cdb1..c026b838db4a 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java
@@ -1053,6 +1053,23 @@ protected void addImport(CodegenModel m, String type) {
}
}
+ /**
+ * Returns true for multipart form arrays whose array or item schema is binary.
+ *
+ * @param parameter Codegen parameter
+ */
+ protected static boolean isBinaryFormArray(CodegenParameter parameter) {
+ if (!parameter.isFormParam || !parameter.isArray) {
+ return false;
+ }
+ if ("binary".equals(parameter.dataFormat)) {
+ return true;
+ }
+
+ CodegenProperty items = parameter.items;
+ return items != null && (items.isFile || items.isBinary || "binary".equals(items.dataFormat));
+ }
+
/**
* Override to fix the inner enum naming issue for maps/arrays of enums.
*
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAxiosClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAxiosClientCodegen.java
index 6852247ea4fa..75e77689776e 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAxiosClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAxiosClientCodegen.java
@@ -230,7 +230,8 @@ private void updateOperationParameterForEnum(OperationsMap operations) {
@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
- if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
+ if (isBinaryFormArray(parameter)) {
+ parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java
index ebba4800f4e2..fd40a7456442 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java
@@ -92,6 +92,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
private static final String X_ENTITY_ID = "x-entityId";
private static final String X_OPERATION_RETURN_PASSTHROUGH = "x-operationReturnPassthrough";
private static final String X_KEEP_AS_JS_OBJECT = "x-keepAsJSObject";
+ private static final String X_TYPESCRIPT_FETCH_API_EXAMPLE = "x-typescriptFetchApiExample";
+ private static final String BLOB_API_EXAMPLE = "new Blob(['example file content'], { type: 'application/octet-stream' })";
protected boolean sagasAndRecords = false;
@Getter @Setter
@@ -416,11 +418,46 @@ public ModelsMap postProcessModels(ModelsMap objs) {
@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
- if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
+ if (isBinaryFormArray(parameter)) {
+ parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}
+ private void addMultipartFileArrayApiExampleValues(OperationsMap operations) {
+ for (CodegenOperation operation : operations.getOperations().getOperation()) {
+ if (operation.allParams == null || operation.allParams.stream().noneMatch(TypeScriptFetchClientCodegen::isBinaryFormArray)) {
+ continue;
+ }
+
+ for (CodegenParameter parameter : operation.allParams) {
+ setApiExampleValue(parameter);
+ }
+ }
+ }
+
+ private void setApiExampleValue(CodegenParameter parameter) {
+ String example = toApiExampleValue(parameter);
+ if (example != null) {
+ parameter.vendorExtensions.put(X_TYPESCRIPT_FETCH_API_EXAMPLE, example);
+ }
+ }
+
+ private String toApiExampleValue(CodegenParameter parameter) {
+ if (isBinaryFormArray(parameter)) {
+ return "[" + BLOB_API_EXAMPLE + "]";
+ } else if (parameter.isFile || parameter.isBinary) {
+ return BLOB_API_EXAMPLE;
+ } else if (parameter.isString) {
+ String example = parameter.example;
+ if (example == null) {
+ example = parameter.paramName + "_example";
+ }
+ return "'" + escapeText(example) + "'";
+ }
+ return null;
+ }
+
@Override
public Map postProcessAllModels(Map objs) {
List allModels = new ArrayList<>();
@@ -746,6 +783,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
}
this.addOperationObjectResponseInformation(operations);
this.addOperationPrefixParameterInterfacesInformation(operations);
+ this.addMultipartFileArrayApiExampleValues(operations);
return operations;
}
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptInversifyClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptInversifyClientCodegen.java
index df055d08eb97..919312ceba29 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptInversifyClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptInversifyClientCodegen.java
@@ -201,7 +201,8 @@ private boolean isLanguageGenericType(String type) {
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
- if (parameter.isFormParam && parameter.isArray && "binary".equals(parameter.dataFormat)) {
+ if (isBinaryFormArray(parameter)) {
+ parameter.isFile = true;
parameter.isCollectionFormatMulti = true;
}
}
diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/api_example.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/api_example.mustache
index b43ddee7da5b..27b7340aab84 100644
--- a/modules/openapi-generator/src/main/resources/typescript-fetch/api_example.mustache
+++ b/modules/openapi-generator/src/main/resources/typescript-fetch/api_example.mustache
@@ -27,7 +27,7 @@ async function example() {
const body = {
{{#allParams}}
// {{{dataType}}}{{#description}} | {{{description}}}{{/description}}{{^required}} (optional){{/required}}
- {{paramName}}: {{{example}}}{{^example}}...{{/example}},
+ {{paramName}}: {{#vendorExtensions.x-typescriptFetchApiExample}}{{{.}}}{{/vendorExtensions.x-typescriptFetchApiExample}}{{^vendorExtensions.x-typescriptFetchApiExample}}{{{example}}}{{^example}}...{{/example}}{{/vendorExtensions.x-typescriptFetchApiExample}},
{{/allParams}}
} satisfies {{operationIdCamelCase}}Request;
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/TypeScriptGroups.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/TypeScriptGroups.java
index e6ceeb26edfa..38ae7ec235fc 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/TypeScriptGroups.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/TypeScriptGroups.java
@@ -22,6 +22,7 @@ public final class TypeScriptGroups {
public static final String TYPESCRIPT_AURELIA = "typescript-aurelia";
public static final String TYPESCRIPT_AXIOS = "typescript-axios";
public static final String TYPESCRIPT_FETCH = "typescript-fetch";
+ public static final String TYPESCRIPT_INVERSIFY = "typescript-inversify";
public static final String TYPESCRIPT_ANGULAR = "typescript-angular";
public static final String TYPESCRIPT_NESTJS = "typescript-nestjs";
public static final String TYPESCRIPT_NODE = "typescript-node";
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/axios/TypeScriptAxiosClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/axios/TypeScriptAxiosClientCodegenTest.java
index e9a3e21e2c39..94d94a4acaf0 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/axios/TypeScriptAxiosClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/axios/TypeScriptAxiosClientCodegenTest.java
@@ -179,6 +179,29 @@ public void testDeprecatedArrayAttribute() throws Exception {
TestUtils.assertFileContains(file, "'nicknames'?: Array");
}
+ @Test(description = "Verify multipart file arrays use repeated form fields")
+ public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
+ final File output = Files.createTempDirectory("typescript_axios_multipart_file_array_").toFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("typescript-axios")
+ .setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ final ClientOptInput clientOptInput = configurator.toClientOptInput();
+ final DefaultGenerator generator = new DefaultGenerator();
+ final List files = generator.opts(clientOptInput).generate();
+ files.forEach(File::deleteOnExit);
+
+ Path api = Paths.get(output + "/api.ts");
+ TestUtils.assertFileExists(api);
+ TestUtils.assertFileContains(api, "files?: Array");
+ TestUtils.assertFileContains(api, "files.forEach((element) => {");
+ TestUtils.assertFileContains(api, "localVarFormParams.append('files', element as any);");
+ TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS.csv)");
+ }
+
@Test
public void generatesTrailingCommasInAsConstEnumObjects() throws Exception {
final File output = Files.createTempDirectory("typescript_axios_trailing_commas_").toFile();
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java
index c21cce633eae..f509a7e13329 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java
@@ -453,6 +453,32 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
}
+ @Test(description = "Verify multipart file arrays use FormData with repeated file fields")
+ public void testMultipartFileArrayUsesFormData() throws IOException {
+ File output = generate(
+ Collections.emptyMap(),
+ "src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml"
+ );
+
+ Path api = Paths.get(output + "/apis/DefaultApi.ts");
+ TestUtils.assertFileExists(api);
+ TestUtils.assertFileContains(api, "files: Array;");
+ TestUtils.assertFileContains(api, "metadata?: string;");
+ TestUtils.assertFileContains(api, "// use FormData to transmit files using content-type \"multipart/form-data\"");
+ TestUtils.assertFileContains(api, "useForm = canConsumeForm;");
+ TestUtils.assertFileContains(api, "formParams = new FormData();");
+ TestUtils.assertFileContains(api, "requestParameters['files'].forEach((element) => {");
+ TestUtils.assertFileContains(api, "formParams.append('files', element as any);");
+ TestUtils.assertFileNotContains(api, "requestParameters['files']!.join(runtime.COLLECTION_FORMATS[\"csv\"])");
+
+ Path apiDocs = Paths.get(output + "/docs/DefaultApi.md");
+ TestUtils.assertFileExists(apiDocs);
+ TestUtils.assertFileContains(apiDocs, "files: [new Blob(['example file content'], { type: 'application/octet-stream' })],");
+ TestUtils.assertFileContains(apiDocs, "metadata: 'metadata_example',");
+ TestUtils.assertFileNotContains(apiDocs, "files: /path/to/file.txt");
+ TestUtils.assertFileNotContains(apiDocs, "metadata: metadata_example");
+ }
+
@Test(description = "Verify instanceOf checks discriminator value for single-value enums")
public void testInstanceOfChecksDiscriminatorValue() throws IOException {
File output = generate(Collections.emptyMap(), "src/test/resources/3_0/typescript-fetch/oneOf.yaml");
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/inversify/TypeScriptInversifyClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/inversify/TypeScriptInversifyClientCodegenTest.java
new file mode 100644
index 000000000000..d6b33a46269c
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/inversify/TypeScriptInversifyClientCodegenTest.java
@@ -0,0 +1,41 @@
+package org.openapitools.codegen.typescript.inversify;
+
+import org.openapitools.codegen.ClientOptInput;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.openapitools.codegen.typescript.TypeScriptGroups;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+@Test(groups = {TypeScriptGroups.TYPESCRIPT, TypeScriptGroups.TYPESCRIPT_INVERSIFY})
+public class TypeScriptInversifyClientCodegenTest {
+
+ @Test(description = "Verify multipart file arrays use repeated form fields")
+ public void testMultipartFileArrayUsesRepeatedFormFields() throws Exception {
+ final File output = Files.createTempDirectory("typescript_inversify_multipart_file_array_").toFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("typescript-inversify")
+ .setInputSpec("src/test/resources/3_0/form-multipart-binary-array.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ final ClientOptInput clientOptInput = configurator.toClientOptInput();
+ final DefaultGenerator generator = new DefaultGenerator();
+ final List files = generator.opts(clientOptInput).generate();
+ files.forEach(File::deleteOnExit);
+
+ Path api = Paths.get(output + "/api/multipart.service.ts");
+ TestUtils.assertFileExists(api);
+ TestUtils.assertFileContains(api, "files?: Array");
+ TestUtils.assertFileContains(api, "files.forEach((element) => {");
+ TestUtils.assertFileContains(api, "formData.append('files', element);");
+ TestUtils.assertFileNotContains(api, "files.join(COLLECTION_FORMATS['csv'])");
+ }
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml
new file mode 100644
index 000000000000..d18433f23086
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml
@@ -0,0 +1,27 @@
+openapi: 3.0.3
+info:
+ title: Multipart File Array
+ version: 1.0.0
+paths:
+ /upload:
+ post:
+ operationId: uploadFiles
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - files
+ properties:
+ files:
+ type: array
+ items:
+ type: string
+ format: binary
+ metadata:
+ type: string
+ responses:
+ '204':
+ description: Successful upload
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator-ignore b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/FILES b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/FILES
new file mode 100644
index 000000000000..4edcf5573756
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/FILES
@@ -0,0 +1,5 @@
+apis/DefaultApi.ts
+apis/index.ts
+docs/DefaultApi.md
+index.ts
+runtime.ts
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/VERSION b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/VERSION
new file mode 100644
index 000000000000..186c33c96ed8
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.24.0-SNAPSHOT
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts b/samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts
new file mode 100644
index 000000000000..88ceccf52fa7
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts
@@ -0,0 +1,95 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Multipart File Array
+ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import * as runtime from '../runtime';
+
+export interface UploadFilesRequest {
+ files: Array;
+ metadata?: string;
+}
+
+/**
+ *
+ */
+export class DefaultApi extends runtime.BaseAPI {
+
+ /**
+ * Creates request options for uploadFiles without sending the request
+ */
+ async uploadFilesRequestOpts(requestParameters: UploadFilesRequest): Promise {
+ if (requestParameters['files'] == null) {
+ throw new runtime.RequiredError(
+ 'files',
+ 'Required parameter "files" was null or undefined when calling uploadFiles().'
+ );
+ }
+
+ const queryParameters: any = {};
+
+ const headerParameters: runtime.HTTPHeaders = {};
+
+ const consumes: runtime.Consume[] = [
+ { contentType: 'multipart/form-data' },
+ ];
+ // @ts-ignore: canConsumeForm may be unused
+ const canConsumeForm = runtime.canConsumeForm(consumes);
+
+ let formParams: { append(param: string, value: any): any };
+ let useForm = false;
+ // use FormData to transmit files using content-type "multipart/form-data"
+ useForm = canConsumeForm;
+ if (useForm) {
+ formParams = new FormData();
+ } else {
+ formParams = new URLSearchParams();
+ }
+
+ if (requestParameters['files'] != null) {
+ requestParameters['files'].forEach((element) => {
+ formParams.append('files', element as any);
+ })
+ }
+
+ if (requestParameters['metadata'] != null) {
+ formParams.append('metadata', requestParameters['metadata'] as any);
+ }
+
+
+ let urlPath = `/upload`;
+
+ return {
+ path: urlPath,
+ method: 'POST',
+ headers: headerParameters,
+ query: queryParameters,
+ body: formParams,
+ };
+ }
+
+ /**
+ */
+ async uploadFilesRaw(requestParameters: UploadFilesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> {
+ const requestOptions = await this.uploadFilesRequestOpts(requestParameters);
+ const response = await this.request(requestOptions, initOverrides);
+
+ return new runtime.VoidApiResponse(response);
+ }
+
+ /**
+ */
+ async uploadFiles(requestParameters: UploadFilesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise {
+ await this.uploadFilesRaw(requestParameters, initOverrides);
+ }
+
+}
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/apis/index.ts b/samples/client/others/typescript-fetch/multipart-file-array/apis/index.ts
new file mode 100644
index 000000000000..69c44c00fa0d
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/apis/index.ts
@@ -0,0 +1,3 @@
+/* tslint:disable */
+/* eslint-disable */
+export * from './DefaultApi';
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md b/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md
new file mode 100644
index 000000000000..78f8ed5c311a
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md
@@ -0,0 +1,77 @@
+# DefaultApi
+
+All URIs are relative to *http://localhost*
+
+| Method | HTTP request | Description |
+|------------- | ------------- | -------------|
+| [**uploadFiles**](DefaultApi.md#uploadfiles) | **POST** /upload | |
+
+
+
+## uploadFiles
+
+> uploadFiles(files, metadata)
+
+
+
+### Example
+
+```ts
+import {
+ Configuration,
+ DefaultApi,
+} from '';
+import type { UploadFilesRequest } from '';
+
+async function example() {
+ console.log("🚀 Testing SDK...");
+ const api = new DefaultApi();
+
+ const body = {
+ // Array
+ files: [new Blob(['example file content'], { type: 'application/octet-stream' })],
+ // string (optional)
+ metadata: 'metadata_example',
+ } satisfies UploadFilesRequest;
+
+ try {
+ const data = await api.uploadFiles(body);
+ console.log(data);
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+// Run the test
+example().catch(console.error);
+```
+
+### Parameters
+
+
+| Name | Type | Description | Notes |
+|------------- | ------------- | ------------- | -------------|
+| **files** | `Array` | | |
+| **metadata** | `string` | | [Optional] [Defaults to `undefined`] |
+
+### Return type
+
+`void` (Empty response body)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+- **Content-Type**: `multipart/form-data`
+- **Accept**: Not defined
+
+
+### HTTP response details
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+| **204** | Successful upload | - |
+
+[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)
+
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/index.ts b/samples/client/others/typescript-fetch/multipart-file-array/index.ts
new file mode 100644
index 000000000000..6aa4c91d3c05
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/index.ts
@@ -0,0 +1,4 @@
+/* tslint:disable */
+/* eslint-disable */
+export * from './runtime';
+export * from './apis/index';
diff --git a/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts
new file mode 100644
index 000000000000..efa9707fc9dd
--- /dev/null
+++ b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts
@@ -0,0 +1,449 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Multipart File Array
+ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
+
+export interface ConfigurationParameters {
+ basePath?: string; // override base path
+ fetchApi?: FetchAPI; // override for fetch implementation
+ middleware?: Middleware[]; // middleware to apply before/after fetch requests
+ queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings
+ username?: string; // parameter for basic security
+ password?: string; // parameter for basic security
+ apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security
+ accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security
+ headers?: HTTPHeaders; //header params we want to use on every request
+ credentials?: RequestCredentials; //value for the credentials param we want to use on each request
+}
+
+export class Configuration {
+ constructor(private configuration: ConfigurationParameters = {}) {}
+
+ set config(configuration: Configuration) {
+ this.configuration = configuration;
+ }
+
+ get basePath(): string {
+ return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
+ }
+
+ get fetchApi(): FetchAPI | undefined {
+ return this.configuration.fetchApi;
+ }
+
+ get middleware(): Middleware[] {
+ return this.configuration.middleware || [];
+ }
+
+ get queryParamsStringify(): (params: HTTPQuery) => string {
+ return this.configuration.queryParamsStringify || querystring;
+ }
+
+ get username(): string | undefined {
+ return this.configuration.username;
+ }
+
+ get password(): string | undefined {
+ return this.configuration.password;
+ }
+
+ get apiKey(): ((name: string) => string | Promise) | undefined {
+ const apiKey = this.configuration.apiKey;
+ if (apiKey) {
+ return typeof apiKey === 'function' ? apiKey : () => apiKey;
+ }
+ return undefined;
+ }
+
+ get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined {
+ const accessToken = this.configuration.accessToken;
+ if (accessToken) {
+ return typeof accessToken === 'function' ? accessToken : async () => accessToken;
+ }
+ return undefined;
+ }
+
+ get headers(): HTTPHeaders | undefined {
+ return this.configuration.headers;
+ }
+
+ get credentials(): RequestCredentials | undefined {
+ return this.configuration.credentials;
+ }
+}
+
+export const DefaultConfig = new Configuration();
+
+/**
+ * This is the base class for all generated API classes.
+ */
+export class BaseAPI {
+
+ private static readonly jsonRegex = /^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$/i;
+ private middleware: Middleware[];
+
+ constructor(protected configuration = DefaultConfig) {
+ this.middleware = configuration.middleware;
+ }
+
+ withMiddleware(this: T, ...middlewares: Middleware[]) {
+ const next = this.clone();
+ next.middleware = next.middleware.concat(...middlewares);
+ return next;
+ }
+
+ withPreMiddleware(this: T, ...preMiddlewares: Array) {
+ const middlewares = preMiddlewares.map((pre) => ({ pre }));
+ return this.withMiddleware(...middlewares);
+ }
+
+ withPostMiddleware(this: T, ...postMiddlewares: Array) {
+ const middlewares = postMiddlewares.map((post) => ({ post }));
+ return this.withMiddleware(...middlewares);
+ }
+
+ /**
+ * Check if the given MIME is a JSON MIME.
+ * JSON MIME examples:
+ * application/json
+ * application/json; charset=UTF8
+ * APPLICATION/JSON
+ * application/vnd.company+json
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
+ * @return True if the given MIME is JSON, false otherwise.
+ */
+ protected isJsonMime(mime: string | null | undefined): boolean {
+ if (!mime) {
+ return false;
+ }
+ return BaseAPI.jsonRegex.test(mime);
+ }
+
+ protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise {
+ const { url, init } = await this.createFetchParams(context, initOverrides);
+ const response = await this.fetchApi(url, init);
+ if (response && (response.status >= 200 && response.status < 300)) {
+ return response;
+ }
+ throw new ResponseError(response, 'Response returned an error code');
+ }
+
+ private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
+ let url = this.configuration.basePath + context.path;
+ if (context.query !== undefined && Object.keys(context.query).length !== 0) {
+ // only add the querystring to the URL if there are query parameters.
+ // this is done to avoid urls ending with a "?" character which buggy webservers
+ // do not handle correctly sometimes.
+ url += '?' + this.configuration.queryParamsStringify(context.query);
+ }
+
+ const headers = Object.assign({}, this.configuration.headers, context.headers);
+ Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
+
+ const initOverrideFn =
+ typeof initOverrides === "function"
+ ? initOverrides
+ : async () => initOverrides;
+
+ const initParams = {
+ method: context.method,
+ headers,
+ body: context.body,
+ credentials: this.configuration.credentials,
+ };
+
+ const overriddenInit: RequestInit = {
+ ...initParams,
+ ...(await initOverrideFn({
+ init: initParams,
+ context,
+ }))
+ };
+
+ let body: any;
+ if (isFormData(overriddenInit.body)
+ || (overriddenInit.body instanceof URLSearchParams)
+ || isBlob(overriddenInit.body)) {
+ body = overriddenInit.body;
+ } else if (this.isJsonMime(headers['Content-Type'])) {
+ body = JSON.stringify(overriddenInit.body);
+ } else {
+ body = overriddenInit.body;
+ }
+
+ const init: RequestInit = {
+ ...overriddenInit,
+ body
+ };
+
+ return { url, init };
+ }
+
+ private fetchApi = async (url: string, init: RequestInit) => {
+ let fetchParams = { url, init };
+ for (const middleware of this.middleware) {
+ if (middleware.pre) {
+ fetchParams = await middleware.pre({
+ fetch: this.fetchApi,
+ ...fetchParams,
+ }) || fetchParams;
+ }
+ }
+ let response: Response | undefined = undefined;
+ try {
+ response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
+ } catch (e) {
+ for (const middleware of this.middleware) {
+ if (middleware.onError) {
+ response = await middleware.onError({
+ fetch: this.fetchApi,
+ url: fetchParams.url,
+ init: fetchParams.init,
+ error: e,
+ response: response ? response.clone() : undefined,
+ }) || response;
+ }
+ }
+ if (response === undefined) {
+ if (e instanceof Error) {
+ throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
+ } else {
+ throw e;
+ }
+ }
+ }
+ for (const middleware of this.middleware) {
+ if (middleware.post) {
+ response = await middleware.post({
+ fetch: this.fetchApi,
+ url: fetchParams.url,
+ init: fetchParams.init,
+ response: response.clone(),
+ }) || response;
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Create a shallow clone of `this` by constructing a new instance
+ * and then shallow cloning data members.
+ */
+ private clone(this: T): T {
+ const constructor = this.constructor as any;
+ const next = new constructor(this.configuration);
+ next.middleware = this.middleware.slice();
+ return next;
+ }
+};
+
+function isBlob(value: any): value is Blob {
+ return typeof Blob !== 'undefined' && value instanceof Blob;
+}
+
+function isFormData(value: any): value is FormData {
+ return typeof FormData !== "undefined" && value instanceof FormData;
+}
+
+export class ResponseError extends Error {
+ override name: "ResponseError" = "ResponseError";
+ constructor(public response: Response, msg?: string) {
+ super(msg);
+
+ // restore prototype chain
+ const actualProto = new.target.prototype;
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(this, actualProto);
+ }
+ }
+}
+
+export class FetchError extends Error {
+ override name: "FetchError" = "FetchError";
+ constructor(public cause: Error, msg?: string) {
+ super(msg);
+
+ // restore prototype chain
+ const actualProto = new.target.prototype;
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(this, actualProto);
+ }
+ }
+}
+
+export class RequiredError extends Error {
+ override name: "RequiredError" = "RequiredError";
+ constructor(public field: string, msg?: string) {
+ super(msg);
+
+ // restore prototype chain
+ const actualProto = new.target.prototype;
+ if (Object.setPrototypeOf) {
+ Object.setPrototypeOf(this, actualProto);
+ }
+ }
+}
+
+export const COLLECTION_FORMATS = {
+ csv: ",",
+ ssv: " ",
+ tsv: "\t",
+ pipes: "|",
+};
+
+export type FetchAPI = WindowOrWorkerGlobalScope['fetch'];
+
+export type Json = any;
+export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
+export type HTTPHeaders = { [key: string]: string };
+export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery };
+export type HTTPBody = Json | FormData | URLSearchParams;
+export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody };
+export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original';
+
+export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise
+
+export interface FetchParams {
+ url: string;
+ init: RequestInit;
+}
+
+export interface RequestOpts {
+ path: string;
+ method: HTTPMethod;
+ headers: HTTPHeaders;
+ query?: HTTPQuery;
+ body?: HTTPBody;
+}
+
+export function querystring(params: HTTPQuery, prefix: string = ''): string {
+ return Object.keys(params)
+ .map(key => querystringSingleKey(key, params[key], prefix))
+ .filter(part => part.length > 0)
+ .join('&');
+}
+
+function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string {
+ const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
+ if (value instanceof Array) {
+ const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue)))
+ .join(`&${encodeURIComponent(fullKey)}=`);
+ return `${encodeURIComponent(fullKey)}=${multiValue}`;
+ }
+ if (value instanceof Set) {
+ const valueAsArray = Array.from(value);
+ return querystringSingleKey(key, valueAsArray, keyPrefix);
+ }
+ if (value instanceof Date) {
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
+ }
+ if (value instanceof Object) {
+ return querystring(value as HTTPQuery, fullKey);
+ }
+ return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
+}
+
+export function exists(json: any, key: string) {
+ const value = json[key];
+ return value !== null && value !== undefined;
+}
+
+export function mapValues(data: any, fn: (item: any) => any) {
+ const result: { [key: string]: any } = {};
+ for (const key of Object.keys(data)) {
+ result[key] = fn(data[key]);
+ }
+ return result;
+}
+
+export function canConsumeForm(consumes: Consume[]): boolean {
+ for (const consume of consumes) {
+ if (consume.contentType?.startsWith('multipart/form-data') == true) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export interface Consume {
+ contentType: string;
+}
+
+export interface RequestContext {
+ fetch: FetchAPI;
+ url: string;
+ init: RequestInit;
+}
+
+export interface ResponseContext {
+ fetch: FetchAPI;
+ url: string;
+ init: RequestInit;
+ response: Response;
+}
+
+export interface ErrorContext {
+ fetch: FetchAPI;
+ url: string;
+ init: RequestInit;
+ error: unknown;
+ response?: Response;
+}
+
+export interface Middleware {
+ pre?(context: RequestContext): Promise;
+ post?(context: ResponseContext): Promise;
+ onError?(context: ErrorContext): Promise;
+}
+
+export interface ApiResponse {
+ raw: Response;
+ value(): Promise;
+}
+
+export interface ResponseTransformer {
+ (json: any): T;
+}
+
+export class JSONApiResponse {
+ constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {}
+
+ async value(): Promise {
+ return this.transformer(await this.raw.json());
+ }
+}
+
+export class VoidApiResponse {
+ constructor(public raw: Response) {}
+
+ async value(): Promise {
+ return undefined;
+ }
+}
+
+export class BlobApiResponse {
+ constructor(public raw: Response) {}
+
+ async value(): Promise {
+ return await this.raw.blob();
+ };
+}
+
+export class TextApiResponse {
+ constructor(public raw: Response) {}
+
+ async value(): Promise {
+ return await this.raw.text();
+ };
+}