Search code examples
nestjsdtoclass-validatorclass-transformer

Nested dto with multipart form data


I need to use a nested Dto in nest-js project

import { Type } from 'class-transformer'
import {
    IsArray,
    IsNotEmpty,
    IsString,
    MaxLength,
    ArrayMaxSize,
    ValidateNested,
} from 'class-validator'

class CreateHighlightBody {
    @IsString()
    @MaxLength(255)
    title: string

    @IsArray()
    @ArrayMaxSize(10)
    content: string[]

    @IsNotEmpty()
    @IsString()
    game: string
}

export class CreateHighlightDto {
    @ValidateNested()
    @Type(() => CreateHighlightBody)
    body: CreateHighlightBody
}

It works fine with common JSON body in request, but if I want to use Multipart Form Data it will give the following error:

{
  "message": [
    "nested property body must be either object or array"
  ],
  "error": "Bad Request",
  "statusCode": 400
}

My controller:

@Post('create')
@Auth()
@UseInterceptors(FileInterceptor('file'))
async create(
    @CurrentUser() user: UserCurrent,
    @Body() dto: CreateHighlightDto,
    @UploadedFile(new ParseFilePipe())
    file: Express.Multer.File
) {
    return this.highlightService.createHighlight(user, file, dto.body)
}

I use insomnia to test API


Solution

  • The solution was to create a pipe to parse the body correctly.

    type TParseFormDataJsonOptions = {
      field: string
    }
    
    export class ParseFormDataJsonPipe implements PipeTransform {
        constructor(private options?: TParseFormDataJsonOptions) {}
    
        transform(value: any) {
            const { field } = this.options
            const jsonField = value[field].replace(
                /(\w+:)|(\w+ :)/g,
                function (matchedStr: string) {
                    return (
                        '"' + matchedStr.substring(0, matchedStr.length - 1) + '":'
                    )
                }
            )
            return JSON.parse(jsonField)
        }
    }
    

    Controller:

    @Post('create')
    @Auth()
    @UseInterceptors(FileInterceptor('file'))
    async create(
      @UploadedFile(new ParseFilePipe({}))
      file: Express.Multer.File,
      @Body(
        new ParseFormDataJsonPipe({ field: 'body' }),
        new ValidationPipe()
      )
      dto: CreateHighlightDto,
      @CurrentUser()
      user: UserCurrent
    ) {
      console.log(dto)
    }
    

    Also changed Dto to:

    export class CreateHighlightDto {
        @IsString()
        @MaxLength(255)
        title: string
    
        @IsArray()
        @ArrayMaxSize(10)
        content: string[]
    
        @IsNotEmpty()
        @IsString()
        game: string
    }