Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java] [typescript-angular] Regression: No model for array of type file #1458

Closed
akehir opened this issue Nov 15, 2018 · 25 comments
Closed

[java] [typescript-angular] Regression: No model for array of type file #1458

akehir opened this issue Nov 15, 2018 · 25 comments

Comments

@akehir
Copy link
Contributor

akehir commented Nov 15, 2018

Description

We have an api, where the resource consumes in the post body an array of files. While I'm not sure if it's fully spec compliant, the swagger codegen generates workable code , but the openapi-generator does not find a model / schema, and uses the UNKNOWN_BASE_TYPE.

Generated Code from openapi-generator
import { UNKNOWN_BASE_TYPE } from '../model/uNKNOWNBASETYPE'; // this can never work, since this type is not generated, and will always end up in a compilation error -> should this just be the any type in typescript?

// [....]
/**
     * uploadFilesToStage
     * 
     * @param uploadId uploadId
     * @param UNKNOWN_BASE_TYPE 
     * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
     * @param reportProgress flag to report request and response progress.
     */
    public uploadFilesToStageUsingPOST(uploadId: string, UNKNOWN_BASE_TYPE: UNKNOWN_BASE_TYPE, observe?: 'body', reportProgress?: boolean): Observable<StagedFileUploadStatus>;
    public uploadFilesToStageUsingPOST(uploadId: string, UNKNOWN_BASE_TYPE: UNKNOWN_BASE_TYPE, observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, UNKNOWN_BASE_TYPE: UNKNOWN_BASE_TYPE, observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, UNKNOWN_BASE_TYPE: UNKNOWN_BASE_TYPE, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
        if (uploadId === null || uploadId === undefined) {
            throw new Error('Required parameter uploadId was null or undefined when calling uploadFilesToStageUsingPOST.');
        }
        if (UNKNOWN_BASE_TYPE === null || UNKNOWN_BASE_TYPE === undefined) {
            throw new Error('Required parameter UNKNOWN_BASE_TYPE was null or undefined when calling uploadFilesToStageUsingPOST.');
        }

        let queryParameters = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        if (uploadId !== undefined && uploadId !== null) {
            queryParameters = queryParameters.set('uploadId', <any>uploadId);
        }

        let headers = this.defaultHeaders;

        // to determine the Accept header
        let httpHeaderAccepts: string[] = [
            '*/*'
        ];
        const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts);
        if (httpHeaderAcceptSelected !== undefined) {
            headers = headers.set('Accept', httpHeaderAcceptSelected);
        }

        // to determine the Content-Type header
        const consumes: string[] = [
            'application/json'
        ];
        const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
        if (httpContentTypeSelected !== undefined) {
            headers = headers.set('Content-Type', httpContentTypeSelected);
        }

        return this.httpClient.post<StagedFileUploadStatus>(`${this.configuration.basePath}/be/files/staged`,
            UNKNOWN_BASE_TYPE,
            {
                params: queryParameters,
                withCredentials: this.configuration.withCredentials,
                headers: headers,
                observe: observe,
                reportProgress: reportProgress
            }
        );
    }
Generated Code from swagger-generator
/**
     * uploadFilesToStage
     * 
     * @param uploadId uploadId
     * @param files files
     * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
     * @param reportProgress flag to report request and response progress.
     */
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'body', reportProgress?: boolean): Observable<StagedFileUploadStatus>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
        if (uploadId === null || uploadId === undefined) {
            throw new Error('Required parameter uploadId was null or undefined when calling uploadFilesToStageUsingPOST.');
        }
        if (files === null || files === undefined) {
            throw new Error('Required parameter files was null or undefined when calling uploadFilesToStageUsingPOST.');
        }

        let queryParameters = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        if (uploadId !== undefined) {
            queryParameters = queryParameters.set('uploadId', <any>uploadId);
        }

        let headers = this.defaultHeaders;

        // to determine the Accept header
        let httpHeaderAccepts: string[] = [
            '*/*'
        ];
        let httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts);
        if (httpHeaderAcceptSelected != undefined) {
            headers = headers.set("Accept", httpHeaderAcceptSelected);
        }

        // to determine the Content-Type header
        let consumes: string[] = [
            'application/json'
        ];

        const canConsumeForm = this.canConsumeForm(consumes);

        let formParams: { append(param: string, value: any): void; };
        let useForm = false;
        let convertFormParamsToString = false;
        if (useForm) {
            formParams = new FormData();
        } else {
            formParams = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        }

        if (files) {
            files.forEach((element) => {
                formParams = formParams.append('files', <any>element) || formParams;
            })
        }

        return this.httpClient.post<StagedFileUploadStatus>(`${this.basePath}/be/files/staged`,
            convertFormParamsToString ? formParams.toString() : formParams,
            {
                params: queryParameters,
                withCredentials: this.configuration.withCredentials,
                headers: headers,
                observe: observe,
                reportProgress: reportProgress
            }
        );
    }
Generated Code from openapi-generator, if consumes is set to multipart/form-data
 /**
     * uploadFilesToStage
     * 
     * @param uploadId uploadId
     * @param files files
     * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
     * @param reportProgress flag to report request and response progress.
     */
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'body', reportProgress?: boolean): Observable<StagedFileUploadStatus>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<StagedFileUploadStatus>>;
    public uploadFilesToStageUsingPOST(uploadId: string, files: Array<Blob>, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
        if (uploadId === null || uploadId === undefined) {
            throw new Error('Required parameter uploadId was null or undefined when calling uploadFilesToStageUsingPOST.');
        }
        if (files === null || files === undefined) {
            throw new Error('Required parameter files was null or undefined when calling uploadFilesToStageUsingPOST.');
        }

        let queryParameters = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        if (uploadId !== undefined && uploadId !== null) {
            queryParameters = queryParameters.set('uploadId', <any>uploadId);
        }

        let headers = this.defaultHeaders;

        // to determine the Accept header
        let httpHeaderAccepts: string[] = [
            '*/*'
        ];
        const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts);
        if (httpHeaderAcceptSelected !== undefined) {
            headers = headers.set('Accept', httpHeaderAcceptSelected);
        }

        // to determine the Content-Type header
        const consumes: string[] = [
            'multipart/form-data'
        ];

        const canConsumeForm = this.canConsumeForm(consumes);

        let formParams: { append(param: string, value: any): any; };
        let useForm = false;
        let convertFormParamsToString = false;
        if (useForm) {
            formParams = new FormData();
        } else {
            formParams = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        }

        if (files) {
            formParams = formParams.append('files', files.join(COLLECTION_FORMATS['csv'])) || formParams;
        }

        return this.httpClient.post<StagedFileUploadStatus>(`${this.configuration.basePath}/be/files/staged`,
            convertFormParamsToString ? formParams.toString() : formParams,
            {
                params: queryParameters,
                withCredentials: this.configuration.withCredentials,
                headers: headers,
                observe: observe,
                reportProgress: reportProgress
            }
        );
    }
Working code example
/**
   * uploadFilesToStage
   *
   * @param uploadId uploadId
   * @param files
   * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
   * @param reportProgress flag to report request and response progress.
   */
  public uploadFilesToStageUsingPOST(uploadId: string, files: Blob[], observe?: 'body', reportProgress?: boolean): Observable<StagedFileUploadStatus>;
  public uploadFilesToStageUsingPOST(uploadId: string, files: Blob[], observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<StagedFileUploadStatus>>;
  public uploadFilesToStageUsingPOST(uploadId: string, files: Blob[], observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<StagedFileUploadStatus>>;
  public uploadFilesToStageUsingPOST(uploadId: string, files: Blob[], observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
    if (uploadId === null || uploadId === undefined) {
      throw new Error('Required parameter uploadId was null or undefined when calling uploadFilesToStageUsingPOST.');
    }
    if (files === null || files === undefined) {
      throw new Error('Required parameter files was null or undefined when calling uploadFilesToStageUsingPOST.');
    }

    let queryParameters = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
    if (uploadId !== undefined && uploadId !== null) {
      queryParameters = queryParameters.set('uploadId', <any>uploadId);
    }

    let headers = this.defaultHeaders;

    // to determine the Accept header
    let httpHeaderAccepts: string[] = [
      '*/*'
    ];
    const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts);
    if (httpHeaderAcceptSelected !== undefined) {
      headers = headers.set('Accept', httpHeaderAcceptSelected);
    }

    // to determine the Content-Type header
    // const consumes: string[] = [
    //   'multipart/form-data'
    // ];

    // const canConsumeForm = this.canConsumeForm(consumes);
    const canConsumeForm = true;

    let formParams: { append(param: string, value: any): any; };
    let useForm = false;
    let convertFormParamsToString = false;
    // use FormData to transmit files using content-type "multipart/form-data"
    // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
    useForm = canConsumeForm;
    if (useForm) {
      formParams = new FormData();
    } // else {
    //   formParams = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
    // }

    if (files !== undefined) {
      // formParams = formParams.append('files', <any>files) || formParams;

      for (var i = 0; i < files.length; i++) {
        formParams = formParams.append('files', files[i]) || formParams;
        // formParams = formParams.append( 'files[' + i + ']', files[i]) || formParams;
      }

      // Array.from(Array(files.length).keys()).map(x => {
      //   formParams.append(`files[${x}]`, files[x]);
      // });
    }

    // Attention: we need to let the browser set the header to automatically configure the file boundaries!
    // const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
    // if (httpContentTypeSelected !== undefined) {
    //   headers = headers.set('Content-Type', httpContentTypeSelected);
    // }

    // console.log('ul', formParams);

    return this.httpClient.post<StagedFileUploadStatus>(`${this.configuration.basePath}/be/files/staged`,
      convertFormParamsToString ? formParams.toString() : formParams,
      {
        params: queryParameters,
        withCredentials: this.configuration.withCredentials,
        headers: headers,
        observe: observe,
        reportProgress: reportProgress
      }
    );
  }
openapi-generator version

openapi-generator: version 3.3.2 -> it works in swagger-codegen 2.3.1

OpenAPI declaration file content or url
{
  "paths": {
      "/be/files/staged": {
      "post": {
        "tags": [
          "atrs-backend-file-upload-controller"
        ],
        "summary": "uploadFilesToStage",
        "operationId": "uploadFilesToStageUsingPOST",
        "consumes": [
          "application/json"
        ],
        "produces": [
          "*/*"
        ],
        "parameters": [
          {
            "name": "uploadId",
            "in": "query",
            "description": "uploadId",
            "required": true,
            "type": "string"
          },
          {
            "name": "files",
            "in": "formData",
            "description": "files",
            "required": true,
            "type": "array",
            "items": {
              "type": "file"
            },
            "collectionFormat": "multi"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "$ref": "#/definitions/StagedFileUploadStatus"
            }
          },
          "201": {
            "description": "Created"
          },
          "401": {
            "description": "Unauthorized"
          },
          "403": {
            "description": "Forbidden"
          },
          "404": {
            "description": "Not Found"
          }
        }
      }
    }
  },
  "definitions": {
      "StagedFileUploadStatus": {
      "type": "object",
      "properties": {
        "fileStatuses": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/FileStatus"
          }
        },
        "uploadId": {
          "type": "string"
        }
      },
      "title": "StagedFileUploadStatus"
    },
    "FileStatus": {
      "type": "object",
      "properties": {
        "accountCount": {
          "type": "integer",
          "format": "int64"
        },
        "accountOwnershipCount": {
          "type": "integer",
          "format": "int64"
        },
        "accountOwnershipUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "accountUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "archiveName": {
          "type": "string"
        },
        "beneficialOwnerAddressCount": {
          "type": "integer",
          "format": "int64"
        },
        "beneficialOwnerAddressUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "beneficialOwnerCount": {
          "type": "integer",
          "format": "int64"
        },
        "beneficialOwnerUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "documentCount": {
          "type": "integer",
          "format": "int64"
        },
        "documentUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "fileName": {
          "type": "string"
        },
        "fileType": {
          "type": "string"
        },
        "messages": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/UiCommunicationMessage"
          }
        },
        "success": {
          "type": "boolean"
        },
        "taxPayerIdentificationNumberCount": {
          "type": "integer",
          "format": "int64"
        },
        "taxPayerIdentificationNumberUpsertCount": {
          "type": "integer",
          "format": "int64"
        },
        "transactionCount": {
          "type": "integer",
          "format": "int64"
        },
        "transactionUpsertCount": {
          "type": "integer",
          "format": "int64"
        }
      },
      "title": "FileStatus"
    }
  }
}
Error message during code-generation
[WARNING] The following schema has undefined (null) baseType. It could be due to form parameter defined in OpenAPI v2 spec with incorrect consumes. A correct 'consumes' for form parameters should be 'application/x-www-form-urlencoded' or 'multipart/form-data'
[WARNING] schema: class Schema {
    title: null
    multipleOf: null
    maximum: null
    exclusiveMaximum: null
    minimum: null
    exclusiveMinimum: null
    maxLength: null
    minLength: null
    pattern: null
    maxItems: null
    minItems: null
    uniqueItems: null
    maxProperties: null
    minProperties: null
    required: [files]
    type: null
    not: null
    properties: {files=class ArraySchema {
        class Schema {
            title: null
            multipleOf: null
            maximum: null
            exclusiveMaximum: null
            minimum: null
            exclusiveMinimum: null
            maxLength: null
            minLength: null
            pattern: null
            maxItems: null
            minItems: null
            uniqueItems: null
            maxProperties: null
            minProperties: null
            required: null
            type: null
            not: null
            properties: null
            additionalProperties: null
            description: files
            format: null
            $ref: null
            nullable: null
            readOnly: null
            writeOnly: null
            example: null
            externalDocs: null
            deprecated: null
            discriminator: null
            xml: null
        }
        type: array
        items: class FileSchema {
            class Schema {
                title: null
                multipleOf: null
                maximum: null
                exclusiveMaximum: null
                minimum: null
                exclusiveMinimum: null
                maxLength: null
                minLength: null
                pattern: null
                maxItems: null
                minItems: null
                uniqueItems: null
                maxProperties: null
                minProperties: null
                required: null
                type: null
                not: null
                properties: null
                additionalProperties: null
                description: null
                format: null
                $ref: null
                nullable: null
                readOnly: null
                writeOnly: null
                example: null
                externalDocs: null
                deprecated: null
                discriminator: null
                xml: null
            }
            type: file
            format: binary
        }
    }}
    additionalProperties: null
    description: null
    format: null
    $ref: null
    nullable: null
    readOnly: null
    writeOnly: null
    example: null
    externalDocs: null
    deprecated: null
    discriminator: null
    xml: null
}
[WARNING] codegenModel is null. Default to UNKNOWN_BASE_TYPE
Command line used for generation

mvn test
mvn verify

The source is spring boot / java, with the following method signature:

@RequestMapping(value = "/staged", method = RequestMethod.POST)
    public StagedFileUploadStatus uploadFilesToStage(@RequestParam String uploadId, @RequestParam("files") MultipartFile[] files) throws IOException { ... }
Steps to reproduce

Generate the code from the json file using openapi-generator generates the incorrect code with an

import { UNKNOWN_BASE_TYPE } from '../model/uNKNOWNBASETYPE';
```.

The swagger-codegen does not create an unknown base type.

##### Suggest a fix/enhancement
- The generated code should never use the unknown base type in typescript. Instead this should just be the <any> type.
- The request signature should not be`foo (uploadId: string, UNKNOWN_BASE_TYPE: UNKNOWN_BASE_TYPE, observe?: 'body', reportProgress?: boolean)` -> the name of the request parameter should still be files.
- Some changes for the generated template (with consumes set to `multipart/form-data`), in order to correctly send the files.

Furthermore, the consumes / content type header should not be set by typescript to 'multipart/form-data' -> this has to be set by the browser (to something like `Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvFl26fEEGB24rwat`). Without the formboundary, the backend is not going to be able to split the form into different files.

I could make a PR with the typescript-angular template changes, however the `UNKNOWN_BASE_TYPE` will result in broken compilation error, and I probably can't make a PR to fix that behaviour. I suggest using an <any> type for this case.

If the consumes is set to `multipart/form-data` (by requiring a List of files, and not an array in the spring boot stuff), some code is generated without the UNKNOWN_BASE_TYPE. However, the problem there is, that the typescript code is incorrect due to some assumptions.

First of all, it generates the following:
```typescript
        if (files) {
            formParams = formParams.append('files', files.join(COLLECTION_FORMATS['csv'])) || formParams;
        }

This will throw TypeError: files.join is not a function. Also, the assumtion that csv is going to be used is incorrect.

Therefore, this should probably be changed to something like the below:

        if (files) {
            for (var i = 0; i < files.length; i++) {
                formParams = formParams.append('files', files[i]) || formParams;
            }
        }

If that's fixed, the next error will be at spring side with org.springframework.web.multipart.MultipartException: Current request is not a multipart request.

        let formParams: { append(param: string, value: any): any; };
        let useForm = false;
        let convertFormParamsToString = false;
        if (useForm) {
            formParams = new FormData();
        } else {
            formParams = new HttpParams({encoder: new CustomHttpUrlEncodingCodec()});
        }

Since useForm is false, the sent content will not be correctly, at least if it's not a text / csv file.

Let me know your thoughts @macjohnny @wing328, I can probably create a template which will hopefully generate a more correct api.

@anthowm
Copy link

anthowm commented Nov 15, 2018

Hi,
I have the same problem in version 3.3.2. I'm going to check in what version this functionality works but in the swagger generator it works as mentioned above.
I've seen that @macjohnny added support for multipart in this MR swagger-api/swagger-codegen#6574 in case it serves to fix this regression.

@akehir
Copy link
Contributor Author

akehir commented Nov 16, 2018

@Clovisforyou : What's exactly your error / issue? The unknown model?

Did you try changing the signature of the .json for the api?
with the following:

   "/be/files/staged": {
      "post": {
        "tags": [
          "atrs-backend-file-upload-controller"
        ],
        "summary": "uploadFilesToStage",
        "operationId": "uploadFilesToStageUsingPOST",
        "consumes": [
          "multipart/form-data"
        ],
        "produces": [
          "*/*"
        ],
        "parameters": [
          {
            "name": "uploadId",
            "in": "query",
            "description": "uploadId",
            "required": true,
            "type": "string"
          },
          {
            "name": "files",
            "in": "formData",
            "description": "files",
            "required": true,
            "type": "array",
            "items": {
              "type": "file"
            },
            "collectionFormat": "multi"
          }
        ],
        "responses": {
          "200": { "description": "OK", "schema": { "$ref": "#/definitions/StagedFileUploadStatus" }},
          "201": {"description": "Created"},
          "401": {"description": "Unauthorized" },
          "403": { "description": "Forbidden"  },
          "404": { "description": "Not Found" } }
      }
    },

To achieve that, the spring boot code had to be changed from Array ([]) to List, as follows:

 @RequestMapping(value = "/staged", method = RequestMethod.POST,consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public StagedFileUploadStatus uploadFilesToStage(@RequestParam String uploadId, @RequestParam List<MultipartFile> files) throws IOException {

The code is actually generated. However, in the Java code / mustache template, it appears that the incorrect code branch is selected.

This generates the following:

if (files) {
            formParams = formParams.append('files', files.join(COLLECTION_FORMATS['csv'])) || formParams;
        }

Looking at the mustache, this should be generated if the collection format is not multi. In the json it's "collectionFormat": "multi", yet the wrong branch is selected:

{{#isCollectionFormatMulti}}
            {{paramName}}.forEach((element) => {
                {{#useHttpClient}}formParams = {{/useHttpClient}}formParams.append('{{baseName}}', <any>element){{#useHttpClient}} || formParams{{/useHttpClient}};
            })
        {{/isCollectionFormatMulti}}
        {{^isCollectionFormatMulti}}
            {{#useHttpClient}}formParams = {{/useHttpClient}}formParams.append('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}'])){{#useHttpClient}} || formParams{{/useHttpClient}};
        {{/isCollectionFormatMulti}}

So I'm not sure, why the collectionFormat is not multi here and the lower branch of the mustache is selected.

@anthowm
Copy link

anthowm commented Nov 16, 2018

I test you json and I have the same issue.
But my issue I guess is related but not exactly the same, you can check it in requestBodies PetMultipart

  "openapi" : "3.0.0",
  "servers" : [ {
    "description" : "SwaggerHub API Auto Mocking",
    "url" : "http://www.localhost:8080"
  } ],
  "info" : {
    "description" : "Sample test Pet API",
    "version" : "1.0.0",
    "title" : "Test Pet API",
    "license" : {
      "name" : "Apache 2.0",
      "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
  "tags" : [ {
    "name" : "pet",
    "description" : "Everything about your Pets"
  } ],
  "paths" : {
    "/pet" : {
      "get" : {
        "tags" : [ "pet" ],
        "summary" : "Get all pets",
        "operationId" : "getPets",
        "responses" : {
          "200" : {
            "description" : "A list of pets",
            "content" : {
              "application/json" : {
                "schema" : {
                  "type" : "array",
                  "items" : {
                    "$ref" : "#/components/schemas/Pet"
                  }
                }
              }
            }
          },
          "500" : {
            "description" : "Internal Server Error"
          }
        }
      },
      "post" : {
        "tags" : [ "pet" ],
        "summary" : "Add a new pet",
        "operationId" : "addPet",
        "responses" : {
          "201" : {
            "description" : "Pet fetched Successfully",
            "content" : {
              "application/json" : {
                "schema" : {
                  "type" : "object",
                  "properties" : {
                    "message" : {
                      "type" : "string"
                    },
                    "pet" : {
                      "$ref" : "#/components/schemas/Pet"
                    }
                  }
                }
              }
            }
          },
          "422" : {
            "description" : "Invalid input"
          },
          "500" : {
            "description" : "Internal Server Error"
          }
        },
        "requestBody" : {
          "$ref" : "#/components/requestBodies/PetMultipart"
        }
      }
    },
    "/pet/{petId}" : {
      "get" : {
        "tags" : [ "pet" ],
        "summary" : "Find pet by ID",
        "description" : "Returns a single pet",
        "operationId" : "getPetById",
        "parameters" : [ {
          "name" : "petId",
          "in" : "path",
          "description" : "ID of pet to return",
          "required" : true,
          "schema" : {
            "type" : "integer",
            "format" : "int64"
          }
        } ],
        "responses" : {
          "200" : {
            "description" : "successful operation",
            "content" : {
              "application/json" : {
                "schema" : {
                  "type" : "object",
                  "properties" : {
                    "messages" : {
                      "type" : "string"
                    },
                    "pet" : {
                      "$ref" : "#/components/schemas/Pet"
                    }
                  }
                }
              }
            }
          },
          "404" : {
            "description" : "Invalid ID supplied"
          },
          "500" : {
            "description" : "Internal Server Error"
          }
        }
      },
      "put" : {
        "tags" : [ "pet" ],
        "summary" : "Update a pet by Id",
        "operationId" : "updatePet",
        "parameters" : [ {
          "name" : "petId",
          "in" : "path",
          "description" : "ID of pet to return",
          "required" : true,
          "schema" : {
            "type" : "integer",
            "format" : "int64"
          }
        } ],
        "responses" : {
          "200" : {
            "description" : "Pet updated Successfully",
            "content" : {
              "application/json" : {
                "schema" : {
                  "type" : "object",
                  "properties" : {
                    "message" : {
                      "type" : "string"
                    },
                    "pet" : {
                      "$ref" : "#/components/schemas/Pet"
                    }
                  }
                }
              }
            }
          },
          "422" : {
            "description" : "Invalid input"
          },
          "500" : {
            "description" : "Internal Server Error"
          }
        },
        "requestBody" : {
          "$ref" : "#/components/requestBodies/PetMultipart"
        }
      },
      "delete" : {
        "tags" : [ "pet" ],
        "summary" : "Deletes a pet",
        "operationId" : "deletePet",
        "parameters" : [ {
          "name" : "petId",
          "in" : "path",
          "description" : "Pet id to delete",
          "required" : true,
          "schema" : {
            "type" : "integer",
            "format" : "int64"
          }
        } ],
        "responses" : {
          "200" : {
            "description" : "Pet deleted"
          },
          "404" : {
            "description" : "Invalid ID supplied"
          },
          "500" : {
            "description" : "Internal Server Error"
          }
        }
      }
    }
  },
  "externalDocs" : {
    "description" : "Find out more about Swagger",
    "url" : "http://swagger.io"
  },
  "components" : {
    "schemas" : {
      "Pet" : {
        "type" : "object",
        "required" : [ "name", "type", "imageUrl" ],
        "properties" : {
          "id" : {
            "type" : "integer",
            "format" : "int64"
          },
          "name" : {
            "type" : "string"
          },
          "type" : {
            "type" : "string"
          },
          "imageUrl" : {
            "type" : "string"
          }
        }
      }
    },
    "requestBodies" : {
      "Pet" : {
        "content" : {
          "application/json" : {
            "schema" : {
              "$ref" : "#/components/schemas/Pet"
            }
          }
        },
        "description" : "Pet object that needs to be added to the store",
        "required" : true
      },
      "PetMultipart" : {
        "content" : {
          "multipart/form-data" : {
            "schema" : {
              "properties" : {
                "name" : {
                  "type" : "string"
                },
                "imageUrl" : {
                  "type" : "string",
                  "format" : "binary"
                }
              }
            }
          }
        },
        "description" : "MultiPart pet",
        "required" : true
      }
    }
  }
}

I will try to investigate more about this.

@anthowm
Copy link

anthowm commented Nov 16, 2018

@akehir Well very strange, with content declared in line I didnt get an error and all work correctly. You are using 2.0 or 3.0 ?

post:
      tags:
        - pet
      summary: Add a new pet
      operationId: addPet
      responses:
        '201':
          description: Pet fetched Successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  pet:
                    $ref: "#/components/schemas/Pet"
        '422':
          description: Invalid input
        '500':
          description: Internal Server Error
      requestBody:
        content: 
          multipart/form-data:
            schema:            
              type: object
              properties:      
                name:           
                  type: string
                type:
                  type: string
                imageUrl:  
                  type: string

@anthowm
Copy link

anthowm commented Nov 18, 2018

The problems that I have detected with this are:

  1. When I declare in yml / json multipart in requestBodies and use it referenced
    UNKNOWN_BASE_TYPE appears like in my first comment.
  2. If you declare the multipart inline everything is generated correctly but the problem of
    TypeError: files.join is not a function appears like @akehir says.
content:
    multipart/form-data:
      schema:
        properties:
          # The property name 'file' will be used for all files.
          file:
            type: array
            items:
              type: string
              format: binary

@akehir
Copy link
Contributor Author

akehir commented Nov 19, 2018

@anthowm:

I'm using swagger / openapi 2.0:

{
  "swagger": "2.0",
}

But in both our cases, the generator goes into the {{^isCollectionFormatMulti}} code branch, which seems to be weird in my opinion.

@macjohnny
Copy link
Member

@akehir I agree that this is a bug, and should be fixed. Do you have time to implement a fix?

@akehir
Copy link
Contributor Author

akehir commented Nov 20, 2018

@macjohnny : If it was just updating the generated client code I'd have made a PR.

The problem is, that I suspect that an incorrect condition is set in the codegen .jar itself. Below, you see the relevant template code. Generated is what's in {{^isCollectionFormatMulti}}, whereas I'd expect {{#isCollectionFormatMulti}} code to be run:

        {{#isCollectionFormatMulti}}
            {{paramName}}.forEach((element) => {
                {{#useHttpClient}}formParams = {{/useHttpClient}}formParams.append('{{baseName}}', <any>element){{#useHttpClient}} || formParams{{/useHttpClient}};
            })
        {{/isCollectionFormatMulti}}
        {{^isCollectionFormatMulti}}
            {{#useHttpClient}}formParams = {{/useHttpClient}}formParams.append('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}'])){{#useHttpClient}} || formParams{{/useHttpClient}};
        {{/isCollectionFormatMulti}}

Furthermore is also that currently the generated API's have let useForm = false; so form uploads are never used.

While I can fix and test a generated angular client well enough, I'm not sure I can provide a fix for the java code itself (or that I'd know if indeed an incorrect code branch is selected).

Furthermore, the petstore API itself does not cover these 2 cases, so there are no existing tests where we could confirm the regression is fixed correctly. It might be sensible to add a new api call with an array of files as well - Although probably most client's don't even test the normal file upload api's. The angular client for sure doesn't test the normal file upload.

See #1458 (comment) for more detail

@akehir
Copy link
Contributor Author

akehir commented Nov 21, 2018

Ok, I took a quick glance at the relevant code. There are 2 relevant code parts: in 1, the code calls 2, and if null is returned by 2, there's a fallback to csv as default in 1. The problem is, that 2 assumes OpenAPI 3.0, which uses form styles, and types 4, and does not handle the case of OpenAPI 2.0, which uses the collectionFormat 3 specification.

I assume the default generator needs to handle both version 2 and version 3 of OpenAPI, and then not return null, which would prevent the default fallback to .csv. The fallback to csv is furthermore only sensible, if explode is set to false (which is not the default for a form, but in all other cases), so a default fallback to 'multi' might be better as well - at the very least in OpenAPI 3.0.

However, reading the below, maybe someone needs to have an in-depth look at the code for the collectionFormat, or at least clean it up?

Also, is my assumption correct, that 2 should handle both OpenAPI 3.0 and 2.0, or how should that be handled?

// TDOO revise collectionFormat
            String collectionFormat = null;
            if (ModelUtils.isArraySchema(parameterSchema)) { // for array parameter
                final ArraySchema arraySchema = (ArraySchema) parameterSchema;
                Schema inner = arraySchema.getItems();
                if (inner == null) {
                    LOGGER.warn("warning! No inner type supplied for array parameter \"" + parameter.getName() + "\", using String");
                    inner = new StringSchema().description("//TODO automatically added by openapi-generator due to missing iner type definition in the spec");
                    arraySchema.setItems(inner);
                }

                collectionFormat = getCollectionFormat(parameter);
                // default to csv:
                collectionFormat = StringUtils.isEmpty(collectionFormat) ? "csv" : collectionFormat;
                CodegenProperty codegenProperty = fromProperty("inner", inner);
                codegenParameter.items = codegenProperty;
                codegenParameter.mostInnerItems = codegenProperty.mostInnerItems;
                codegenParameter.baseType = codegenProperty.dataType;
                codegenParameter.isContainer = true;
                codegenParameter.isListContainer = true;


                // recursively add import
                while (codegenProperty != null) {
                    imports.add(codegenProperty.baseType);
                    codegenProperty = codegenProperty.items;
                }

            } else if (ModelUtils.isMapSchema(parameterSchema)) { // for map parameter
                CodegenProperty codegenProperty = fromProperty("inner", ModelUtils.getAdditionalProperties(parameterSchema));
                codegenParameter.items = codegenProperty;
                codegenParameter.mostInnerItems = codegenProperty.mostInnerItems;
                codegenParameter.baseType = codegenProperty.dataType;
                codegenParameter.isContainer = true;
                codegenParameter.isMapContainer = true;

                // recursively add import
                while (codegenProperty != null) {
                    imports.add(codegenProperty.baseType);
                    codegenProperty = codegenProperty.items;
                }
            }
/* TODO revise the logic below
            } else {
                Map<PropertyId, Object> args = new HashMap<PropertyId, Object>();
                String format = qp.getFormat();
                args.put(PropertyId.ENUM, qp.getEnum());
                property = PropertyBuilder.build(type, format, args);
            }
*/

            CodegenProperty codegenProperty = fromProperty(parameter.getName(), parameterSchema);
            // TODO revise below which seems not working
            //if (parameterSchema.getRequired() != null && !parameterSchema.getRequired().isEmpty() && parameterSchema.getRequired().contains(codegenProperty.baseName)) {
            codegenProperty.required = Boolean.TRUE.equals(parameter.getRequired()) ? true : false;
            //}
            //codegenProperty.required = true;

@macjohnny
Copy link
Member

@akehir thanks for the analysis. Maybe @wing328 or @jmini can help out here? I guess they know best about openapi 2.0 vs 3.0

@macjohnny
Copy link
Member

@jimschubert could you help here please?

@anthowm
Copy link

anthowm commented Dec 13, 2018

Hi guys any update about this ?

@jimschubert
Copy link
Member

jimschubert commented Dec 14, 2018

Thanks for bumping this. I didn't see that I was tagged above.

I'll reach out and see if someone knows if this is being fixed elsewhere.

@wing328
Copy link
Member

wing328 commented Dec 14, 2018

I'll take a look later today.

@wing328
Copy link
Member

wing328 commented Dec 14, 2018

        "consumes": [
          "application/json"
        ],
        "produces": [
          "*/*"
        ],
        "parameters": [
          {
            "name": "uploadId",
            "in": "query",
            "description": "uploadId",
            "required": true,
            "type": "string"
          },
          {
            "name": "files",
            "in": "formData",
            "description": "files",
            "required": true,
            "type": "array",
            "items": {
              "type": "file"
            },
            "collectionFormat": "multi"
          }
        ],

I don't think the consumes (application/json) is correct. It should be multipart/form, right?

            "name": "files",
            "in": "formData",
            "description": "files",
            "required": true,
            "type": "array",
            "items": {
              "type": "file"
            },
            "collectionFormat": "multi"

What does the payload looks like? Heads up: file in OAS v2 will be replaced by type: string, format: binary in OAS v3.

When you said it works fine with swagger codegen 2.3.1. Do you only mean the code generation does not throw error/warning? or you're referring to the auto-generated client able to upload multiple files through POST operation to the endpoint?

@wing328
Copy link
Member

wing328 commented Dec 14, 2018

with content declared in line I didnt get an error and all work correctly.

@anthowm please declare it inline for the time being as content does not support reference at the moment.

@anthowm
Copy link

anthowm commented Dec 15, 2018

I currently use OAS 3.0 this is my yml for 1 image only (Generator version 3.3.3

requestBody:
        content: 
          multipart/form-data:
            schema:
              properties:
                name:
                  type: string
                type:
                  type: string
                imageUrls:
                    type: string
                    format: binary

With this spec I get something like (name?: string, type?: string, imageUrls?: Blob, observe?: 'events', reportProgress?: boolean) in my typescript service

For Array

requestBody:
        content: 
          multipart/form-data:
            schema:
              properties:
                name:
                  type: string
                type:
                  type: string
                imageUrls:
                    type: array
                    items:
                        type: string
                        format: binary

With this spec I get something like (name?: string, type?: string, imageUrls?: Blob[], observe?: 'events', reportProgress?: boolean) in my typescript service

This time I tested, it send to my server as [object File] so I guess now is working but my server get empty array ( I'm using express so maybe I miss something I will update you ASAP).

image

When you said it works fine with swagger codegen 2.3.1. Do you only mean the code generation does not throw error/warning? I mean to this. I get same error later.

@wing328
Copy link
Member

wing328 commented Dec 17, 2018

@anthowm can you ping me via https://gitter.im (ID: wing328) when you've time this week to work together on the issue?

My timezone is +0800.

@akehir
Copy link
Contributor Author

akehir commented Dec 17, 2018

@wing328 : Is it ok if I contact you tomorrow as well?

The question is, is it required to use openapi 3.0 specification instead of openapi 2.0? As I'm saying, the variable is set incorrectly / the openapi 2.0 code results in an incorrect default behaviour and is not parsed at all.

You can see in my first comment (here: #1458 (comment) ) the client code generated by swagger-codegen compared to openapi-codegen.

In swagger, the unknown_base type issue is not occurring, furthermore the files are appended in a better way than in openapi generator:

 if (files) {
            files.forEach((element) => {
                formParams = formParams.append('files', <any>element) || formParams;
            })
        }

@anthowm :
Try deleting the following part of your code (this needs to be empty for webkit to set the header correctly:

// to determine the Content-Type header
        const consumes: string[] = [
            'multipart/form-data'
        ];

Because, if it's empty, the browser will correctly say that the form boundary is the token (----WebKitFormBoundary....), and only then the server will accept it. The reason for you receiving an empty array is, because the openapi generator (as well as the swagger generator) incorrectly manually set this particular header where they shouldn't.

As soon as the issue with OpenAPI 2.0 selecting the wrong code branch is fixed, I can create a pull request removing this code, which should then work.

@anthowm
Copy link

anthowm commented Dec 18, 2018

@akehir
If I comment that it works :D.
But I'll wait until it's fixed because I auto generate the code and I upload it to npm to consume it and at the moment I do not hurry. Thank you very much for your time.

@macjohnny
Copy link
Member

any update here? the issue still exists in the current version

@anthowm
Copy link

anthowm commented Jan 26, 2020

This is working with OAS 3

@TranDuyLong8119
Copy link

I still get this issue when trying to generate to 'typescript-angular' Can anyone help me?
image

  • openApi: 3.0.1
  • swagger codegen 4.3.1

@hasparus
Copy link

Any tips how to deal with UNKNOWN_BASE_TYPE? I get it both in typescript-fetch and typescript-node.

@wing328
Copy link
Member

wing328 commented May 13, 2022

I think this should be fixed in the latest master. Please pull the latest to give it a try.

@wing328 wing328 closed this as completed May 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants