Skip to content

[BUG][typescript-fetch] Additional-Properties/Collections in Multipart-Requests #22568

@sermler

Description

@sermler

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

Using a multipart request body which has a collection as part generates invalid code for the typescript-fetch client. I think it was introduced with #20923.

Spec below creates following for example:

/* tslint:disable */
/* eslint-disable */
/**
 * Minimal
 * Api to reproduce bug
 *
 * The version of the OpenAPI document: 0.5.1-SNAPSHOT.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 CreateFileRequest {
    documentBytes: Blob;
    documentType: string;
    properties: { [key: string]: string; };
}

/**
 * 
 */
export class FileApi extends runtime.BaseAPI {

    /**
     */
    async createFileRaw(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
        if (requestParameters['documentBytes'] == null) {
            throw new runtime.RequiredError(
                'documentBytes',
                'Required parameter "documentBytes" was null or undefined when calling createFile().'
            );
        }

        if (requestParameters['documentType'] == null) {
            throw new runtime.RequiredError(
                'documentType',
                'Required parameter "documentType" was null or undefined when calling createFile().'
            );
        }

        if (requestParameters['properties'] == null) {
            throw new runtime.RequiredError(
                'properties',
                'Required parameter "properties" was null or undefined when calling createFile().'
            );
        }

        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['documentBytes'] != null) {
            formParams.append('documentBytes', requestParameters['documentBytes'] as any);
        }

        if (requestParameters['documentType'] != null) {
            formParams.append('documentType', requestParameters['documentType'] as any);
        }

        if (requestParameters['properties'] != null) {
            formParams.append('properties', new Blob([JSON.stringify({ [key: string]: string; }ToJSON(requestParameters['properties']))], { type: "application/json", }));
                    }


        let urlPath = `/api/v1/file`;

        const response = await this.request({
            path: urlPath,
            method: 'POST',
            headers: headerParameters,
            query: queryParameters,
            body: formParams,
        }, initOverrides);

        return new runtime.VoidApiResponse(response);
    }

    /**
     */
    async createFile(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
        await this.createFileRaw(requestParameters, initOverrides);
    }

}

but should look like

/* tslint:disable */
/* eslint-disable */
/**
 * Minimal
 * Api to reproduce bug
 *
 * The version of the OpenAPI document: 0.5.1-SNAPSHOT.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 CreateFileRequest {
    documentBytes: Blob;
    documentType: string;
    properties: { [key: string]: string; };
}

/**
 * 
 */
export class FileApi extends runtime.BaseAPI {

    /**
     */
    async createFileRaw(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
        if (requestParameters['documentBytes'] == null) {
            throw new runtime.RequiredError(
                'documentBytes',
                'Required parameter "documentBytes" was null or undefined when calling createFile().'
            );
        }

        if (requestParameters['documentType'] == null) {
            throw new runtime.RequiredError(
                'documentType',
                'Required parameter "documentType" was null or undefined when calling createFile().'
            );
        }

        if (requestParameters['properties'] == null) {
            throw new runtime.RequiredError(
                'properties',
                'Required parameter "properties" was null or undefined when calling createFile().'
            );
        }

        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['documentBytes'] != null) {
            formParams.append('documentBytes', requestParameters['documentBytes'] as any);
        }

        if (requestParameters['documentType'] != null) {
            formParams.append('documentType', requestParameters['documentType'] as any);
        }

        if (requestParameters['properties'] != null) {
            formParams.append('properties', new Blob([JSON.stringify(requestParameters['properties'])], { type: "application/json", }));
        }


        let urlPath = `/api/v1/file`;

        const response = await this.request({
            path: urlPath,
            method: 'POST',
            headers: headerParameters,
            query: queryParameters,
            body: formParams,
        }, initOverrides);

        return new runtime.VoidApiResponse(response);
    }

    /**
     */
    async createFile(requestParameters: CreateFileRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
        await this.createFileRaw(requestParameters, initOverrides);
    }

}
openapi-generator version

7.17.0

OpenAPI declaration file content or url
openapi: 3.0.3
info:
  title: Minimal
  description: Api to reproduce bug
  version: 0.5.1-SNAPSHOT.0
tags:
  - name: test
servers:
  - url: http://localhost:8080
paths:
  "/api/v1/file":
    post:
      tags:
        - file
      operationId: createFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/FileUploadRequest"
            encoding:
              documentBytes:
                contentType: "*/*"
              properties:
                contentType: application/json
      responses:
        "201":
          description: File created successfully
components:
  schemas:
    FileUploadRequest:
      type: object
      properties:
        documentBytes:
          type: string
          format: binary
        documentType:
          type: string
        properties:
          $ref: '#/components/schemas/TypeMap'
      required:
        - documentBytes
        - documentType
        - properties
    TypeMap:
        type: object
        additionalProperties:
          type: string

<--
(for YAML code) or

(here your code)

(for JSON code), so it becomes more readable. If it is longer than about ten lines,
please create a Gist (https://gist.github.com) or upload it somewhere else and
link it here.
-->

Generation Details
{
  "$schema": "https://raw.githubusercontent.com/OpenAPITools/openapi-generator-cli/refs/heads/master/apps/generator-cli/src/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "7.17.0",
    "generators": {
      "typescript-client-sdk": {
        "generatorName": "typescript-fetch",
        "inputSpec": "api-specification.yml",
        "output": "#{cwd}/sdks/typescript-client-sdk",
        "additionalProperties": {
          "enumPropertyNaming": "original",
          "enumUnknownDefaultCase": true
        }
      }
    }
  }
}

npx @openapitools/openapi-generator-cli generate

Steps to reproduce

See config and spec above.

Related issues/PRs

#20195

Suggest a fix

I am going to prepare a PR.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions