NSwag icon indicating copy to clipboard operation
NSwag copied to clipboard

Multipart/form-data with two parts does not generate correct typescript

Open jonmill opened this issue 7 years ago • 15 comments

I have a C# method (prototype below) that contains one Object to be serialized to JSON and one file upload. No matter what option I try (JSON object as Form Data or as Body data), the JSON object is either not valid or not part of the generated typescript. This is using version 11.12.9.

When using [FromData] to incorporate the object as a multipart form, the generated typescript calls toString() on the object, which uses the Object prototype and therefore adds [object Object] to the call, which is not valid.

When using [FromBody] for the object, the object is entirely ignored by the generated typescript. It is part of the method signature but is never added to the HTTP request.

Providing method signature, Swagger JSON, and output Typescript below for reference.

Prototype

[HttpPost]
[AllowAnonymous]
[Consumes("multipart/form-data")]
[ActionName("applyForJob")]
[SwaggerResponse((int)HttpStatusCode.OK, null)]
[SwaggerResponse((int)HttpStatusCode.BadRequest, typeof(void))]
public async Task<IActionResult> ApplyForJob(
      [FromForm] JobApplicationModel jobModel,
      [FromForm] IFormFile resume)

Swagger

"/api/Contact/applyForJob": {
      "post": {
        "tags": [
          "Contact"
        ],
        "operationId": "Contact_applyForJob",
        "consumes": [
          "multipart/form-data"
        ],
        "parameters": [
          {
            "type": "object",
            "name": "jobModel",
            "in": "formData",
            "x-schema": {
              "$ref": "#/definitions/JobApplicationModel"
            },
            "x-nullable": true
          },
          {
            "type": "file",
            "name": "resume",
            "in": "formData",
            "x-nullable": true
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "400": {
            "description": ""
          }
        },
        "security": [
          {
            "apiKey": []
          }
        ]
      }
    },

Typescript

apiContactApplyforjob(jobModel: JobApplicationModel | null, resume: FileParameter | null): Observable<void> {
        let url_ = this.baseUrl + "/api/Contact/applyForJob";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = new FormData();
        if (jobModel !== null && jobModel !== undefined)
            content_.append("jobModel", jobModel.toString());
        if (resume !== null && resume !== undefined)
            content_.append("resume", resume.data, resume.fileName ? resume.fileName : "resume");
....[snipped for clarity]

jonmill avatar Nov 27 '17 09:11 jonmill

When using [FromBody] for the object, the object is entirely ignored by the generated typescript. It is part of the method signature but is never added to the HTTP request.

Any updates on this? I experience the above issue with this 2.0 swagger spec:

"parameters": [
          {
            "name": "fileUrl",
            "in": "query",
            "description": "REDACTED",
            "required": false,
            "type": "string"
          },
          {
            "name": "thumbnail",
            "in": "formData",
            "description": "REDACTED",
            "required": false,
            "type": "file"
          },
          {
            "in": "body",
            "name": "videoNew",
            "description": "videoNew",
            "required": true,
            "schema": {
              "$ref": "REDACTED"
            }
          }
        ]

Generated typescript code:

fileUploadUsingPOST(videoNew: REDACTED, fileUrl?: string | null | undefined, thumbnail?: FileParameter | null | undefined): Observable<REDACTED> {
        let url_ = this.baseUrl + "/REDACTED?";
        if (fileUrl !== undefined)
            url_ += "fileUrl=" + encodeURIComponent("" + fileUrl) + "&"; 
        url_ = url_.replace(/[?&]$/, "");

        const content_ = new FormData();
        if (thumbnail !== null && thumbnail !== undefined)
            content_.append("thumbnail", thumbnail.data, thumbnail.fileName ? thumbnail.fileName : "thumbnail");

        let options_ : any = {
            body: content_,
            method: "post",
            headers: new Headers({
                "Accept": "application/json"
            })
        };
[....]

The videoNewparameter is never used.

FynnMazurkiewicz avatar Jun 15 '18 14:06 FynnMazurkiewicz

I'd like to help, I assume this error also lives in the template files? Specifically, Client.RequestBody.liquid:22-31 seems to be the starting this issue:

{%     if operation.HasContent -%}
{%         if operation.ContentParameter.IsXmlBodyParameter -%}
const content_ = {{ operation.ContentParameter.VariableName }};
{%         else -%}
const content_ = JSON.stringify({{ operation.ContentParameter.VariableName }});
{%         endif -%}
{%     else -%}
const content_ = undefined;
{%     endif -%}
{% endif -%}

The template itself looks correct. The length of operation.FormParameters is 1. So probably template variables are parsed incorrectly? Feels like 'In': 'formData' takes over 'In': 'body'entirely. But I might be wrong, since I am just guessing at variable names here.

I am more than happy to give this a spin, if you point me in the right direction.

FynnMazurkiewicz avatar Jun 15 '18 14:06 FynnMazurkiewicz

@RicoSuter Is there any progress to this issue? A fix which wasn´t posted here?

Have the same problem and need to fix it. Any help would highly appreciated.

ChristophWieske avatar Apr 15 '19 13:04 ChristophWieske

Is it allowed to have body and formData parameters at the same time?

RicoSuter avatar Apr 15 '19 13:04 RicoSuter

Isn´t formData just a special format of the body?

In my case probably allowed, although I don´t know how to accomplish.

ChristophWieske avatar Apr 15 '19 13:04 ChristophWieske

If you meant form url encoded: Its way to much data for a query in my scenario.

ChristophWieske avatar Apr 15 '19 13:04 ChristophWieske

I'm running into the same issue here. Trying to generate an OAuth client based on:

    [Consumes("application/x-www-form-urlencoded")]
    [Produces("application/json")]
    [HttpPost("~" + TokenPath)]
    public async Task<IActionResult> Exchange([FromForm] OpenIdConnectRequest request)

Which gives:

exchange(request: OpenIdConnectRequest | null | undefined): Observable<FileResponse | null> {
    let url_ = this.baseUrl + "/authorization/connect/token";
    url_ = url_.replace(/[?&]$/, "");

    let content_ = "";
    if (request !== undefined)
        content_ += encodeURIComponent("request") + "=" + encodeURIComponent("" + request) + "&"; 
    content_ = content_.replace(/&$/, "");

I guess either the request members should be separately encoded, or the FromData parameters should be exploded into separate arguments which would fix the problem too.

Then, there's still the issue of possible nested objects. So if you want to be really safe, you'd still have to cater for that somehow (URLSearchParams could help: https://github.com/angular/angular/issues/7370)

GrimaceOfDespair avatar Apr 21 '19 08:04 GrimaceOfDespair

@jonmill I run into the same problem as you described and manage to go around it by overriding toString() method of that specific type.

import * as generated from "./Client";

class CreateEditViewModel extends generated.CreateEditViewModel {
        public toString() {
        return JSON.stringify(this);
     }
}

I used this as guidance https://github.com/RicoSuter/NJsonSchema/wiki/TypeScriptGenerator#extended-classes-and-extension-code

Hope this will help somebody in the same boat!

EugenRajkovic avatar May 19 '20 11:05 EugenRajkovic

I'm working on the form-data stuff at the moment... please also have a look at the latest build on master, maybe this already solves your problem:

You can download the latest build artifacts (e.g. the NSwagStudio) here:

https://ci.appveyor.com/project/rsuter/nswag-25x6o/build/artifacts

RicoSuter avatar May 19 '20 15:05 RicoSuter

Any update on this? I am still experiencing this using version 13.7.0.

C# code
public async Task<ActionResult<CreateProjectResponse>> Create(
    [FromBody] CreateProjectRequest request, 
    IFormFile previewImage, 
    List<IFormFile> images)
Generated TypeScript
create(request: CreateProjectRequest, previewImage: FileParameter | null | undefined, images: FileParameter[] | null | undefined): Observable<CreateProjectResponse> {
        let url_ = this.baseUrl + "/api/v1/projects";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = new FormData();
        if (previewImage !== null && previewImage !== undefined)
            content_.append("previewImage", previewImage.data, previewImage.fileName ? previewImage.fileName : "previewImage");
        if (images !== null && images !== undefined)
            images.forEach(item_ => content_.append("images", item_.data, item_.fileName ? item_.fileName : "images") );

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json",
                "Accept": "application/json"
            })
        };

        return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
            return this.processCreate(response_);
        })).pipe(_observableCatch((response_: any) => {
            if (response_ instanceof HttpResponseBase) {
                try {
                    return this.processCreate(<any>response_);
                } catch (e) {
                    return <Observable<CreateProjectResponse>><any>_observableThrow(e);
                }
            } else
                return <Observable<CreateProjectResponse>><any>_observableThrow(response_);
        }));
    }

As you can see, the request: CreateProjectRequest is part of the generated method, but it's never used anywhere in the body of the method and never added to the form data.

jnsn avatar Sep 03 '20 06:09 jnsn

Adding support for that is probably a multi-day job - ie implement it, avoid breaking changes in existing scenarios, add lot of tests.. currently I do not have the time to do this in my free time. As a workaround you can "hide" the method in the generator and add your own custom/manual implementation via extension code.

RicoSuter avatar Sep 11 '20 12:09 RicoSuter

Any news on this? I am still experiencing this using version 13.10.1.0

cluen avatar Jan 27 '21 15:01 cluen

Any news on this? Looks like still exising issue..

markosimulak avatar Oct 20 '23 13:10 markosimulak

problem still exists

DreadfulBot avatar Oct 24 '23 03:10 DreadfulBot

This post has helped me handling a request with an IFormFile + an object (deserialized from JSON) on the .net core api side. Maybe could help you as well

jpnant avatar Jan 31 '24 10:01 jpnant