【问题标题】:NestJs: How to have Body input shape different from entity's DTO?NestJs:如何使 Body 输入形状与实体的 DTO 不同?
【发布时间】:2025-12-27 21:35:12
【问题描述】:

我的照片和标签对象具有如下所示的 DTO:

export class PhotoDto {
    readonly title: string
    readonly file: string
    readonly tags: TagDto[]
}

export class TagDto {
    readonly name: string
}

我在我的photo.service.ts 中使用PhotoDto 并最终在photo.controller.ts 中创建照片:

// In photo.service.ts
async create(createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
   return await this.photoRepo.create(createPhotoDto)
}

// In photo.controller.ts
@Post()
async create(@Body() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
}

但是,API 正文中的输入应具有以下结构:

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": [
      {
         "name": "holiday"
      },
      {
         "name": "memories"
      }
   ]
}

如何更改Body 的输入形状以改为接受此结构?

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": ["holiday", "memories"]
}

我尝试创建 2 个不同的 DTO,一个 CreatePhotoDto 和一个 InputPhotoDto,一个用于控制器中所需的输入形状,一个用于服务和实体,但这最终会非常混乱,因为有一个在 2 个 DTO 之间进行转换的工作量很大。

Post 请求的Body 获得不同的输入形状,然后将其转换为实体使用所需的 DTO 的正确方法是什么?

【问题讨论】:

    标签: javascript node.js typescript serialization nestjs


    【解决方案1】:

    您可以使用ValidationPipe()的自动转换:

    1) 将ValidationPipe 添加到您的控制器:

    @UsePipes(new ValidationPipe({ transform: true }))
    @Post()
    async create(@Body() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
       // ...
    }
    

    2) 将@Transform 添加到您的PhotoDto

    // Transforms string[] to TagDto[]
    const transformTags = tags => {
      if (Array.isArray(tags)) {
        return tags.map(tag => ({name: tag}))
      } else {
        return tags;
      }
    }
    
    
    import { Transform } from 'class-transformer';
    export class PhotoDto {
        readonly title: string
        readonly file: string
        @Transform(transformTags, {toClassOnly: true})
        readonly tags: TagDto[]
    }
    

    【讨论】:

      【解决方案2】:

      将 DTO 更新为

      export class PhotoDto { readonly title: string readonly file: string readonly tags: Array<string> }

      它将API结构更改为

      { "title": "Photo Title", "file": "/some/path/file.jpg", "tags": ["holiday", "memories"] }

      当前您的 tags 属性是 TagDto 类型的对象数组,请将 tags 属性更改为字符串数组。

      【讨论】:

        【解决方案3】:

        您可以创建一个nest 自定义装饰器来将输入数据转换为您的 DTO 对象。

        export const ConvertToCreateCatDto = createRouteParamDecorator((data, req): CreateCatDto => { // `createParamDecorator` for nest old version
            if (req.body.tags.every(value => typeof value === "string")) { // if input tags is a string[]
                req.body.tags = (req.body.tags as string[]).map<TagDto>((tag) => {
                    return { // convert to TagDto
                        name: tag + ""
                    }
                });
            }
            let result = new CreateCatDto(req.body);
            // TODO: validate `result` object
            return result;
        });
        

        向 CreateCatDto 添加构造函数

        export class CreateCatDto {
            readonly title: string;
            readonly file: number;
            readonly tags: TagDto[];
        
            constructor(obj: any) {
                this.title = obj.title;
                this.file = obj.file;
                this.tags = obj.tags;
            }
        }
        

        最后,在你的控制器中使用@ConvertToCreateCatDto 而不是@Body

        // In photo.controller.ts
        @Post()
        async create(@ConvertToCreateCatDto() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
           //...
        }
        

        【讨论】: