如何在NestJs中验证FormData提交的对象数组?
NestJS 接口接收FormData中对象数组的问题
服务器端代码
接口定义
@Post() @UseInterceptors( FileFieldsInterceptor([ { name: 'settingsFile', maxCount: 1 }, { name: 'imageFile', maxCount: 1} ]) ) async create( @Body() createCaptionDto: CreatePostDto, @UploadedFiles() files: { settingsFile: Express.Multer.File[]; imageFile: Express.Multer.File[]; } ) {...
DTO定义
export class CreatePostDto { ... @IsArray() @ArrayMinSize(1) // @ValidateNested({ each: true }) // @Type(() => PostSource) sources: Array<PostSource>; @IsArray() @IsString({ each: true }) tags: Array<string>; }
前端调用问题
前端使用fetch+FormData传输数据时,字符串数组tags通过循环append('tags[]', tag)可正常解析,但对象数组sources的多种尝试均无法正确反序列化为对象数组:
- 尝试一:
formData.append('sources[]', JSON.stringify(sources));
服务器解析结果:
['[{attr1: 223, attr2: 3737}, {attr1: 235325, attr2: 5366}]']
字符串化的数组被额外包裹一层数组,不符合预期。
- 尝试二:
for (const source of createCaptionDto.sources) { formData.append('sources[]', JSON.stringify(source)); }
服务器解析结果:
['{attr1: 223, attr2: 3737}', '{attr1: 235325, attr2: 5366}']
得到的是字符串数组而非对象数组。
- 尝试三:
formData.append('sources', JSON.stringify(sources));
NestJS无法识别该字段为数组类型。
解决办法
方案1:启用DTO嵌套验证+拆分属性传参
首先在CreatePostDto中启用嵌套验证(取消之前注释的代码):
export class CreatePostDto { ... @IsArray() @ArrayMinSize(1) @ValidateNested({ each: true }) // 启用嵌套对象验证 @Type(() => PostSource) // 指定嵌套对象的类型 sources: Array<PostSource>; @IsArray() @IsString({ each: true }) tags: Array<string>; }
前端传输时,将对象的每个属性按sources[index].property的格式单独添加到FormData:
createCaptionDto.sources.forEach((source, index) => { // 示例:假设PostSource包含attr1、attr2两个属性 formData.append(`sources[${index}].attr1`, source.attr1); formData.append(`sources[${index}].attr2`, source.attr2); });
这种方式下,NestJS的class-validator和class-transformer会自动将参数解析为PostSource对象数组。
方案2:自定义管道处理字符串反序列化
保留前端尝试二的传参方式,在服务器端添加自定义管道解析字符串为对象:
- 创建
ParseJsonArrayPipe管道:
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class ParseJsonArrayPipe implements PipeTransform { transform(value: any) { if (!Array.isArray(value)) { throw new BadRequestException('sources必须为数组格式'); } try { return value.map(item => JSON.parse(item)); } catch (error) { throw new BadRequestException('sources数组中存在无效JSON格式的元素'); } } }
- 在接口中使用该管道:
async create( @Body('sources', new ParseJsonArrayPipe()) sources: PostSource[], @Body() createCaptionDto: Omit<CreatePostDto, 'sources'>, @UploadedFiles() files: { ... } ) { const fullDto = { ...createCaptionDto, sources }; // 后续业务逻辑 }
也可以直接在DTO的sources字段上使用@Transform装饰器简化处理:
import { Transform } from 'class-transformer'; export class CreatePostDto { ... @IsArray() @ArrayMinSize(1) @Transform(({ value }) => value.map(item => JSON.parse(item))) sources: Array<PostSource>; ... }
方案3:将非文件数据作为单个JSON字段传输
前端把除文件外的所有数据序列化为JSON字符串,作为单个字段传入FormData:
// 把整个DTO序列化为JSON字符串 formData.append('payload', JSON.stringify(createCaptionDto)); // 单独添加文件 formData.append('settingsFile', settingsFile); formData.append('imageFile', imageFile);
服务器端调整接收逻辑:
async create( @Body('payload') payload: string, @UploadedFiles() files: { ... } ) { const createCaptionDto = JSON.parse(payload) as CreatePostDto; // 若需要验证,可手动调用class-validator的validate方法 // 后续业务逻辑 }
这种方式无需修改DTO验证规则,实现简单直接,但需手动处理JSON解析和可能的参数验证。
内容的提问来源于stack exchange,提问作者HSE
相关产品推荐
相关产品推荐

