openapi-generator: [java] [typescript-angular] Regression: No model for array of type file
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.
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 4
- Comments: 25 (15 by maintainers)
Any tips how to deal with UNKNOWN_BASE_TYPE? I get it both in
typescript-fetchandtypescript-node.@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: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 https://github.com/OpenAPITools/openapi-generator/issues/1458#issuecomment-439348229 for more detail