From c3730b5f37392a7a4b81351fdad4518b14d75dc0 Mon Sep 17 00:00:00 2001 From: hwisu Date: Thu, 25 Jun 2026 23:29:53 +0900 Subject: [PATCH 1/5] Align TypeScript multipart file array handling --- .../AbstractTypeScriptClientCodegen.java | 12 ++++++ .../TypeScriptAxiosClientCodegen.java | 3 +- .../TypeScriptFetchClientCodegen.java | 3 +- .../TypeScriptInversifyClientCodegen.java | 3 +- .../codegen/typescript/TypeScriptGroups.java | 1 + .../TypeScriptAxiosClientCodegenTest.java | 23 +++++++++++ .../TypeScriptFetchClientCodegenTest.java | 19 +++++++++ .../TypeScriptInversifyClientCodegenTest.java | 41 +++++++++++++++++++ .../multipart-file-array.yaml | 27 ++++++++++++ 9 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/inversify/TypeScriptInversifyClientCodegenTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/typescript-fetch/multipart-file-array.yaml 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..e707d4fba929 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,18 @@ protected void addImport(CodegenModel m, String type) { } } + 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..5c54b70e06aa 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 @@ -416,7 +416,8 @@ 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; } } 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/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..2bb4dd808ae1 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,25 @@ 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\"])"); + } + @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 From 6a867ec4b097b34d541442a24f82717ee0ebf835 Mon Sep 17 00:00:00 2001 From: hwisu Date: Thu, 25 Jun 2026 23:30:40 +0900 Subject: [PATCH 2/5] Add TypeScript fetch multipart file array sample --- ...typescript-fetch-multipart-file-array.yaml | 4 + .../.openapi-generator-ignore | 23 + .../.openapi-generator/FILES | 5 + .../.openapi-generator/VERSION | 1 + .../multipart-file-array/apis/DefaultApi.ts | 95 ++++ .../multipart-file-array/apis/index.ts | 3 + .../multipart-file-array/docs/DefaultApi.md | 76 +++ .../multipart-file-array/index.ts | 4 + .../multipart-file-array/runtime.ts | 449 ++++++++++++++++++ 9 files changed, 660 insertions(+) create mode 100644 bin/configs/typescript-fetch-multipart-file-array.yaml create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator-ignore create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/FILES create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/.openapi-generator/VERSION create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/apis/index.ts create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/index.ts create mode 100644 samples/client/others/typescript-fetch/multipart-file-array/runtime.ts 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/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..522af343cde5 --- /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..144d157b465d --- /dev/null +++ b/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md @@ -0,0 +1,76 @@ +# 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: /path/to/file.txt, + // 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..465be21fe150 --- /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(); + }; +} From b67964ad623a5ff300dd46492b24e10d86d88700 Mon Sep 17 00:00:00 2001 From: hwisu Date: Fri, 26 Jun 2026 10:44:35 +0900 Subject: [PATCH 3/5] Document TypeScript binary form array helper --- .../codegen/languages/AbstractTypeScriptClientCodegen.java | 5 +++++ 1 file changed, 5 insertions(+) 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 e707d4fba929..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,11 @@ 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; From 7d32880de85f6ab3c3763452cd59cb37b350b1ef Mon Sep 17 00:00:00 2001 From: hwisu Date: Fri, 26 Jun 2026 13:16:06 +0900 Subject: [PATCH 4/5] Fix TypeScript fetch multipart docs example --- .../TypeScriptFetchClientCodegen.java | 25 +++++++++++++++++++ .../typescript-fetch/api_example.mustache | 2 +- .../TypeScriptFetchClientCodegenTest.java | 7 ++++++ .../multipart-file-array/docs/DefaultApi.md | 4 +-- 4 files changed, 35 insertions(+), 3 deletions(-) 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 5c54b70e06aa..1f50f7f3a0e8 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 @@ -420,6 +422,29 @@ public void postProcessParameter(CodegenParameter parameter) { parameter.isFile = true; parameter.isCollectionFormatMulti = true; } + 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 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/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java index 2bb4dd808ae1..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 @@ -470,6 +470,13 @@ public void testMultipartFileArrayUsesFormData() throws IOException { 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") 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 index 144d157b465d..5ed6d5ac4fe2 100644 --- a/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md +++ b/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md @@ -29,9 +29,9 @@ async function example() { const body = { // Array - files: /path/to/file.txt, + files: [new Blob(['example file content'], { type: 'application/octet-stream' })], // string (optional) - metadata: metadata_example, + metadata: 'metadata_example', } satisfies UploadFilesRequest; try { From cd404a472c2a8c4fe5622d0be80c2c196e6bcb36 Mon Sep 17 00:00:00 2001 From: hwisu Date: Fri, 26 Jun 2026 13:58:18 +0900 Subject: [PATCH 5/5] Limit TypeScript fetch multipart docs examples --- .../languages/TypeScriptFetchClientCodegen.java | 14 +++++++++++++- .../multipart-file-array/apis/DefaultApi.ts | 4 ++-- .../multipart-file-array/docs/DefaultApi.md | 1 + .../multipart-file-array/runtime.ts | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) 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 1f50f7f3a0e8..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 @@ -422,7 +422,18 @@ public void postProcessParameter(CodegenParameter parameter) { parameter.isFile = true; parameter.isCollectionFormatMulti = true; } - setApiExampleValue(parameter); + } + + 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) { @@ -772,6 +783,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L } this.addOperationObjectResponseInformation(operations); this.addOperationPrefixParameterInterfacesInformation(operations); + this.addMultipartFileArrayApiExampleValues(operations); return operations; } 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 index 522af343cde5..88ceccf52fa7 100644 --- a/samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts +++ b/samples/client/others/typescript-fetch/multipart-file-array/apis/DefaultApi.ts @@ -5,7 +5,7 @@ * 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 @@ -20,7 +20,7 @@ export interface UploadFilesRequest { } /** - * + * */ export class DefaultApi extends runtime.BaseAPI { 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 index 5ed6d5ac4fe2..78f8ed5c311a 100644 --- a/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md +++ b/samples/client/others/typescript-fetch/multipart-file-array/docs/DefaultApi.md @@ -74,3 +74,4 @@ No authorization required | **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/runtime.ts b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts index 465be21fe150..efa9707fc9dd 100644 --- a/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts +++ b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts @@ -5,7 +5,7 @@ * 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