Swagger: How to upload file with additional form data

Created on 5 Nov 2018  路  16Comments  路  Source: nestjs/swagger

I'm submitting a...


[ ] Regression 
[ ] Bug report
[ ] Feature request
[ X ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

I'm using nestjs/swagger and trying to figure out a way to upload a file and also pass additional form data along with the image.

I'm using this to upload the image, and it works fine.

  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiImplicitFile({ name: 'file', required: true })
  uploadFile(@UploadedFile() file) {
    console.log('got upload');
    console.log('...file', file);
  }

However, I'd like to include some meta data about my file as form data. I tried using @Body(), but that expects a json payload, and the upload requires multipart/form-data.

  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiImplicitFile({ name: 'file', required: true })
  uploadFile(@UploadedFile() file, @Body() body: MyDto) {
    console.log('got upload');
    console.log('...file', file);
    console.log('...body', body);
  }

I also tried ApiImplicitBody, but couldn't seem to get it to work. I'm just guessing because I seem to be deep in undocumented territory.

What is the correct way to:

  • Access other properties of the multi-part form (beyond the file)
  • Define that as part of the swagger definition with the swagger decorators
enhancement

Most helpful comment

I experience a similar problem. The file upload does work for me using multipart/form-data, and I am successfully sending additional properties with it:

@Post()
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@ApiImplicitFile({ name: 'file', required: true })
async uploadModel(@UploadedFile() file, @Body() modelData: CreateModelDto) {
  // (...)
}

All additional properties are passed to the modelData when sending a proper POST request from the application.
The issue is that swagger does not understand it and the "Try it out" does not work. It sends just the file without any additional data.

screenshot from 2018-11-13 12-53-01
screenshot from 2018-11-13 12-53-24

All 16 comments

I experience a similar problem. The file upload does work for me using multipart/form-data, and I am successfully sending additional properties with it:

@Post()
@UseInterceptors(FileInterceptor('file'))
@ApiConsumes('multipart/form-data')
@ApiImplicitFile({ name: 'file', required: true })
async uploadModel(@UploadedFile() file, @Body() modelData: CreateModelDto) {
  // (...)
}

All additional properties are passed to the modelData when sending a proper POST request from the application.
The issue is that swagger does not understand it and the "Try it out" does not work. It sends just the file without any additional data.

screenshot from 2018-11-13 12-53-01
screenshot from 2018-11-13 12-53-24

I made a pull request addressing this issue:
https://github.com/nestjs/swagger/pull/168

Fixed in 3.1.0

i have been reading on @nestjs/swagger version 3.1.0
and it no support additional form data,
if you want genarate document for this you need to write a custom decorator like this.

api-implicit-form-data.decorator.ts

import { createParamDecorator } from '@nestjs/swagger/dist/decorators/helpers';
import { isNil } from 'lodash';

const initialMetadata = {
    name: '',
    required: true,
};

export const ApiImplicitFormData = (metadata: {
    name: string;
    description?: string;
    required?: boolean;
    type: any;
}): MethodDecorator => {
    const param = {
        name: isNil(metadata.name) ? initialMetadata.name : metadata.name,
        in: 'formData',
        description: metadata.description || '',
        required: metadata.required || false,
        type: metadata.type,
    };
    return createParamDecorator(param, initialMetadata);
};

and using it ad controller function like this:
test.controller.ts
@ApiOperation({ title: 'Upload Photo Campaign' }) @ApiConsumes('multipart/form-data') @ApiImplicitFormData({ name: 'imageData', required: true, type: 'file' }) @ApiImplicitFormData({ name: 'imageInfo', required: true, type: String }) @ApiImplicitFormData({ name: 'campaignId', required: true, type: String }) doPhotoCampaignUpload( @Req() req: Request, @Res() res: Response, @UploadedFile() file, @Body() uploadInfo, ) { try { Logger.log(file); Logger.log(uploadInfo); return res.status(HttpStatus.OK).json({ status: 200, data: 'On contructing', }); } catch (error) { return res.status(HttpStatus.OK).json({ status: -999, data: 'error', }); } } }
i tested on my project, and it working fine
hope it help.

@see https://github.com/nestjs/swagger/pull/573

To generate correct OpenApi 3 swagger.json you should be able to use:

    @Post('upload')
      @ApiConsumes('multipart/form-data')
      @UseInterceptors(FileInterceptor('file'))
      @ApiBody({
        type: 'multipart/form-data',
        required: true,
        schema: {
          type: 'object',
          properties: {
            file: {
              type: 'string',
              format: 'binary'
            }
          }
        }
      })
      uploadFile(@UploadedFile() file) {
        return file;
      }

You can create a custom dto with the file param like
import { ApiProperty } from "@nestjs/swagger";

export class UserDto{
   name: string;
   @ApiProperty({type:"file"})
   avatar?: any;

}

and it makes some sense, because the file is part of the data to be transferred

In case you want to upload _multiple files_ in ^4.6.1 you can use custom dto like

export class ProductDTO {
  @ApiProperty({ description: 'Product name' })
  name: string;

  @ApiProperty({
    description: 'Attachments',
    type: 'array',
    items: {
      type: 'file',
      items: {
        type: 'string',
        format: 'binary',
      },
    },
  })
  files: any[];
}

then use it on your controller

@Post('/product')
@ApiConsumes('multipart/form-data')
@UseInterceptors(FilesInterceptor('files')) 
public async product(
  @Body() payload: ProductDTO,
  @UploadedFiles() files,
) {
   return await this.svc.product(payload, files);
}

Screen Shot 2020-11-13 at 11 28 08 AM

I made couple of decorator for this purpose:

export const ApiFile = (options?: ApiPropertyOptions): PropertyDecorator => (
  target: Object,
  propertyKey: string | symbol,
) => {
  if (options?.isArray) {
    ApiProperty({
      type: 'array',
      items: {
        type: 'file',
        properties: {
          [propertyKey]: {
            type: 'string',
            format: 'binary',
          },
        },
      },
    })(target, propertyKey);
  } else {
    ApiProperty({
      type: 'file',
      properties: {
        [propertyKey]: {
          type: 'string',
          format: 'binary',
        },
      },
    })(target, propertyKey);
  }
};

@Injectable()
export class FilesToBodyInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const req = ctx.getRequest();
    if (req.body && Array.isArray(req.files) && req.files.length) {
      req.files.forEach((file: Express.Multer.File) => {
        const { fieldname } = file;
        if (!req.body[fieldname]) {
          req.body[fieldname] = [file];
        } else {
          req.body[fieldname].push(file);
        }
      });
    }

    return next.handle();
  }
}

@Injectable()
export class FileToBodyInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const req = ctx.getRequest();
    if (req.body && req.file?.fieldname) {
      const { fieldname } = req.file;
      if (!req.body[fieldname]) {
        req.body[fieldname] = req.file;
      }
    }

    return next.handle();
  }
}

And I use it in the models like this:

export class SingleFileFormDataDTO {
  @ApiProperty()
  id: string;

  @ApiProperty()
  description: string;

  @ApiFile()
  image: Express.Multer.File;
}

export class MultipleFilesFormDataDTO {
  @ApiProperty()
  id: string;

  @ApiProperty()
  description: string;

  @ApiFile({ isArray: true })
  images: Express.Multer.File[];
}

And in the controller:

@Controller('my_route')
export class MyController {
  @ApiBody({ type: SingleFileFormDataDTO })
  @Post('single')
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('image'), FileToBodyInterceptor)
  async uploadSingleFile(
    @Body() body: SingleFileFormDataDTO,
  ): Promise<void> {
    if (!body.image) {
      throw new BadRequestException('no image file sent.');
    }
    return;
  }


  @ApiBody({ type: MultipleFilesFormDataDTO })
  @Post('multiple')
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FilesInterceptor('images'), FilesToBodyInterceptor)
  async uploadMultipleFiles(
    @Body() body: MultipleFilesFormDataDTO,
  ): Promise<void> {
    if (!body.images) {
      throw new BadRequestException('no image file sent.');
    }
    return;
  }

}

I guess the interceptors and the property decorator should be in nestjs core, does any one from maintainers thinks it worth to make a PR?

maybe, because the issue is Closed, it's not too visible, @kamilmysliwiec as a newcomer to Nest.js, I think it totally worth the effort to at least be documented under the https://docs.nestjs.com/openapi/ section. I would gladly contribute to that, but I have no idea which of the suggested solutions are "preferred", or "idiomatic" to Nest.js..

@kamilmysliwiec I facing the same issue as well. I would like to upload multiple files with different field names with additional form data on a single end point. Is that possible using swagger as of now?

@mushroomgenie if you do the following as above, it will be what you get on swagger:

image

Hi @karianpour , I'm having some trouble with your proposed solution since the @Body() body: MultipleFilesFormDataDTO, is always empty.

I'm configuring the Nest app with the following code:

  dService.use(
    json({
      limit: jsonLimit,
    }),
  );
  dService.use(urlencoded({ extended: true, limit: urlEncodedLimit }));

My request is something like:

POST <base_url>/api/v1/media/upload
Authorization: Bearer {{authToken}}
Accept: application/json
Content-Type: multipart/form-data; boundary=MyBoundary

--MyBoundary
Content-Disposition: form-data; name="file"; filename="test.http"
Content-Type: application/http

<file_content>
--MyBoundary
Content-Disposition: form-data; name="body"
Content-Type: application/json

{"foo": "bar"}
--MyBoundary--

Do you know why the body is empty?

Thank you.

@massimeddu I checked the request that works with my implementation as it is as follow:

Content-Disposition: form-data; name="id"

1234
------WebKitFormBoundaryBmgnZTxe1QsTRSK3
Content-Disposition: form-data; name="description"

description data here
------WebKitFormBoundaryBmgnZTxe1QsTRSK3
Content-Disposition: form-data; name="images"; filename="0.png"
Content-Type: image/png

<file content>
------WebKitFormBoundaryBmgnZTxe1QsTRSK3
Content-Disposition: form-data; name="images"; filename="20210108_211248#1.jpg"
Content-Type: image/jpeg


<file content>
------WebKitFormBoundaryBmgnZTxe1QsTRSK3--

I guess that you have some problem on the function decorators, here I explain more:

@ApiConsumes('multipart/form-data')  // this decorator reads the data (files and fields), and sets files to the request.files property (not body) and sets the fields to body.
@UseInterceptors(FilesInterceptor('images'), FilesToBodyInterceptor) // attention to the 's',



md5-219143406414a8a0cdda5a93ea79ca86



@ApiConsumes('multipart/form-data')
@UseInterceptors(FileInterceptor('file'), FileToBodyInterceptor)
@UseInterceptors(FileInterceptor('body'), FileToBodyInterceptor)



md5-30b47b01c49c1f6a7c0451b819deed65



  async uploadFileAndJson(
    @Request() req,
  ): Promise<void> {
    console.log(req);
    console.log(req.files);
  }

Thanks @karianpour , it worked!

I've changed my request in this way:

--MyBoundary
Content-Disposition: form-data; name="file"; filename="test.http"
Content-Type: application/http

<filecontent>
--MyBoundary
Content-Disposition: form-data; name="data"

{"id": 1, "displayLabel": "data"}
--MyBoundary--

Then I've added this new Interceptor:

import { CallHandler, ExecutionContext, NestInterceptor, Type, mixin } from '@nestjs/common';
import { Observable } from 'rxjs';

export function JsonToObjectsInterceptor(fields: string[]): Type<NestInterceptor> {
  class JsonToObjectsInterceptorClass implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
      const request = context.switchToHttp().getRequest();

      if (request.body) {
        fields.forEach((field) => {
          if (request.body[field]) {
            request.body[field] = JSON.parse(request.body[field]);
          }
        });
      }
      return next.handle();
    }
  }
  const Interceptor = mixin(JsonToObjectsInterceptorClass);
  return Interceptor as Type<NestInterceptor>;
}

And I've applied it:

UseInterceptors(FileInterceptor('file'), FileToBodyInterceptor, JsonToObjectsInterceptor(['data']))

Finally I'm now able to access to body.file and body.data on my Controller.

Thank you for your help!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

taipoxin picture taipoxin  路  4Comments

dennisameling picture dennisameling  路  4Comments

alisherks picture alisherks  路  4Comments

KatSick picture KatSick  路  3Comments

mogusbi picture mogusbi  路  3Comments