NestJS在DTO创建期间执行验证之前,使用ValidationPipe转换属性

NestJS在DTO创建期间执行验证之前,使用ValidationPipe转换属性,validation,nestjs,dto,class-validator,class-transformer,Validation,Nestjs,Dto,Class Validator,Class Transformer,我使用内置的NestJS ValidationPipe以及类验证器和类转换器来验证和清理入站JSON主体有效负载。我面临的一种情况是,在入站JSON对象中混合使用大小写属性名。我想纠正这些属性,并将其映射到我们新的TypeScript NestJS API中的标准驼峰式模型,这样我就不会将遗留系统中不匹配的模式耦合到我们的新API和新标准,本质上使用DTO中的@Transform作为应用程序其余部分的隔离机制。例如,入站JSON对象的属性: "propertyone", &q

我使用内置的NestJS ValidationPipe以及类验证器和类转换器来验证和清理入站JSON主体有效负载。我面临的一种情况是,在入站JSON对象中混合使用大小写属性名。我想纠正这些属性,并将其映射到我们新的TypeScript NestJS API中的标准驼峰式模型,这样我就不会将遗留系统中不匹配的模式耦合到我们的新API和新标准,本质上使用DTO中的@Transform作为应用程序其余部分的隔离机制。例如,入站JSON对象的属性:

"propertyone",
"PROPERTYTWO",
"PropertyThree"
应该映射到

"propertyOne",
"propertyTwo",
"propertyThree"
我想使用@Transform来实现这一点,但我认为我的方法不正确。我想知道是否需要编写自定义验证管道。这是我目前的做法

控制器:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { TestMeRequestDto } from './testmerequest.dto';

@Controller('test')
export class TestController {
  constructor() {}

  @Post()
  @UsePipes(new ValidationPipe({ transform: true }))
  async get(@Body() testMeRequestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    const response = do something useful here... ;
    return response;
  }
}
{
  "propertyone":"test1",
  "PROPERTYTWO":"test2",
  "PropertyThree":"test3,
  "simpleModel":{"sometestproperty":"test4"}
}
TestMeRequestDto:

import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { TestMeModel } from './testme.model';

export class TestMeRequestDto {
  @IsNotEmpty()
  @Transform((propertyone) => propertyone.valueOf())
  propertyOne!: string;

  @IsNotEmpty()
  @Transform((PROPERTYTWO) => PROPERTYTWO.valueOf())
  propertyTwo!: string;

  @IsNotEmpty()
  @Transform((PropertyThree) => PropertyThree.valueOf())
  propertyThree!: string;

  @ValidateNested({ each: true })
  @Type(() => TestMeModel)
  simpleModel!: TestMeModel

}
用于发送到控制器的样本有效负载:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { TestMeRequestDto } from './testmerequest.dto';

@Controller('test')
export class TestController {
  constructor() {}

  @Post()
  @UsePipes(new ValidationPipe({ transform: true }))
  async get(@Body() testMeRequestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    const response = do something useful here... ;
    return response;
  }
}
{
  "propertyone":"test1",
  "PROPERTYTWO":"test2",
  "PropertyThree":"test3,
  "simpleModel":{"sometestproperty":"test4"}
}
我遇到的问题是:

这些变换似乎没有效果。类验证器告诉我这些属性中的每一个都不能为空。例如,如果我将propertyone更改为propertyone,那么类验证器对该属性的验证就可以了,例如,它可以看到值。其他两个属性也是如此。若我对它们进行了验证,那个么类验证器会很高兴。这是在验证发生之前转换未运行的症状吗? 这个很奇怪。当我调试和评估TestMeRequestDto对象时,我可以看到simpleModel属性包含一个包含属性名sometestproperty的对象,即使TestMeModel的类定义有一个camelcase sometestproperty。为什么@Type=>TestMeModel不尊重该属性名的正确大小写?test4的值存在于该属性中,因此它知道如何理解该值并分配该值。 非常奇怪的是,TestMeModel上someTestProperty属性的@IsNotEmpty验证没有失败,例如,它看到了test4值并满足了要求,即使示例JSON负载中的入站属性名称是someTestProperty,这都是小写。
如果社区有任何见解和指导,我们将不胜感激。谢谢

您可能需要使用类transformer文档的部分。本质上,您的@Transform需要如下所示:

从“类验证器”导入{IsNotEmpty,ValidateNested}; 从“类转换器”导入{Transform,Type}; 从“./testme.model”导入{TestMeModel}; 导出类TestMeRequestDto{ @不空虚 @Transformvalue,obj=>obj.propertyone.valueOf propertyOne!:字符串; @不空虚 @Transformvalue,obj=>obj.PROPERTYTWO.valueOf propertyTwo!:字符串; @不空虚 @Transformvalue,obj=>obj.propertytree.valueOf PropertyTree!:字符串; @ValidateNested{each:true} @类型=>TestMeModel simpleModel!:TestMeModel } 这将使传入的有效负载为

{ 物业名称:价值1, 物业2:价值2, 不动产树:价值3, } 并将其转化为您设想的DTO

编辑日期:2020年12月30日 因此,我最初使用@Transform的想法并不像预想的那样有效,这真是一个令人沮丧的想法,因为它看起来很不错。因此,您可以做的不是很枯燥,但它仍然与类转换器一起工作,这是一个胜利。通过使用@Exclude和@Expose,您可以使用属性访问器作为奇怪命名属性的别名,如下所示:

类更正为{ @暴露 获得财产{ 返回此.propertyONE; } @暴露 获取属性two:string{ 返回此.PROPERTYTWO; } @暴露 获取属性树:字符串{ 返回此.propertytree; } @排除{toPlainOnly:true} 属性:字符串; @排除{toPlainOnly:true} 属性二:字符串; @排除{toPlainOnly:true} 属性树:字符串; } 现在您可以访问dto.propertyOne并获得所需的属性,当您使用classToPlain时,如果您使用的是Nest的序列化拦截器,它将删除propertyOne和其他属性。否则,在辅助管道中,您可以将NewDTO、classToPlainvalue明码标价,其中NewDTO仅包含正确的字段

您可能还需要研究的另一件事是,看看它是否有更好的功能来处理类似的事情


如果您感兴趣,

您可能需要使用类transformer文档的部分。本质上,您的@Transform需要如下所示:

从“类验证器”导入{IsNotEmpty,ValidateNested}; 从“类转换器”导入{Transform,Type}; 从“./testme.model”导入{TestMeModel}; 导出类TestMeRequestDto{ @不空虚 @Transformvalue,obj=>obj.propertyone.valueOf propertyOne!:字符串; @不空虚 @Transformvalue,obj=>obj.PROPERTYTWO.valueOf propertyTwo!:字符串; @不空虚 @Transfo rmvalue,obj=>obj.propertytree.valueOf 地产树!:一串 @ValidateNested{each:true} @类型=>TestMeModel simpleModel!:测试备忘录 } 这将使传入的有效负载为

{ 物业名称:价值1, 物业2:价值2, 不动产树:价值3, } 并将其转化为您设想的DTO

编辑日期:2020年12月30日 因此,我最初使用@Transform的想法并不像预想的那样有效,这真是一个令人沮丧的想法,因为它看起来很不错。因此,您可以做的不是很枯燥,但它仍然与类转换器一起工作,这是一个胜利。通过使用@Exclude和@Expose,您可以使用属性访问器作为奇怪命名属性的别名,如下所示:

类更正为{ @暴露 获得财产{ 返回此.propertyONE; } @暴露 获取属性two:string{ 返回此.PROPERTYTWO; } @暴露 获取属性树:字符串{ 返回此.propertytree; } @排除{toPlainOnly:true} 属性:字符串; @排除{toPlainOnly:true} 属性二:字符串; @排除{toPlainOnly:true} 属性树:字符串; } 现在您可以访问dto.propertyOne并获得所需的属性,当您使用classToPlain时,如果您使用的是Nest的序列化拦截器,它将删除propertyOne和其他属性。否则,在辅助管道中,您可以将NewDTO、classToPlainvalue明码标价,其中NewDTO仅包含正确的字段

您可能还需要研究的另一件事是,看看它是否有更好的功能来处理类似的事情


如果您感兴趣,

作为Jay execellent答案的替代方案,您还可以创建一个自定义管道,在其中保存将请求负载映射/转换为所需DTO的逻辑。可以这么简单:

export class RequestConverterPipe implements PipeTransform{
    transform(body: any, metadata: ArgumentMetadata): TestMeRequestDto {
        const result = new TestMeRequestDto();
        // can of course contain more sophisticated mapping logic
        result.propertyOne = body.propertyone;
        result.propertyTwo = body.PROPERTYTWO;
        result.propertyThree = body.PropertyThree;
        return result;
    }

export class TestMeRequestDto {
    @IsNotEmpty()
    propertyOne: string;
    @IsNotEmpty()
    propertyTwo: string;
    @IsNotEmpty()
    propertyThree: string;
}
然后,您可以在控制器中这样使用它,但需要确保顺序正确,即RequestConverterPipe必须在ValidationPipe之前运行,这也意味着ValidationPipe不能全局设置:

@UsePipes(new RequestConverterPipe(), new ValidationPipe())
async post(@Body() requestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    // ...
}

作为Jay execellent答案的替代方案,您还可以创建一个自定义管道,在其中保存将请求负载映射/转换为所需DTO的逻辑。可以这么简单:

export class RequestConverterPipe implements PipeTransform{
    transform(body: any, metadata: ArgumentMetadata): TestMeRequestDto {
        const result = new TestMeRequestDto();
        // can of course contain more sophisticated mapping logic
        result.propertyOne = body.propertyone;
        result.propertyTwo = body.PROPERTYTWO;
        result.propertyThree = body.PropertyThree;
        return result;
    }

export class TestMeRequestDto {
    @IsNotEmpty()
    propertyOne: string;
    @IsNotEmpty()
    propertyTwo: string;
    @IsNotEmpty()
    propertyThree: string;
}
然后,您可以在控制器中这样使用它,但需要确保顺序正确,即RequestConverterPipe必须在ValidationPipe之前运行,这也意味着ValidationPipe不能全局设置:

@UsePipes(new RequestConverterPipe(), new ValidationPipe())
async post(@Body() requestDto: TestMeRequestDto): Promise<TestMeResponseDto> {
    // ...
}

这是另一个好办法。需要注意的是,您必须在ValidationPipe之前运行RequestConverterPipe,这意味着无法全局设置ValidationPipe。是的,这是真的-好的一点。嘿,Eol,感谢您的响应!我可能需要创建一个定制的PipeTransform类,如图所示。尽管我非常喜欢下面Jay示例中DTO中的单线转换,但我遇到的问题是,当入站属性的大小写与DTO属性的大小写不匹配时,我无法将对象和值参数放入转换中。我只是不知道是哪一层造成了这个问题。我把你的回答标记为答案,因为这似乎是最简单的前进之路。虽然DTO上的Exclude和Expose装饰器可以工作,但我觉得我在给DTO增加额外的重量,希望尽可能多地保留DTO中的逻辑。我希望在体系结构中保持清晰的分隔线,使DTO尽可能精简,以履行其作为传输实体的角色,不执行转换,并将转换操作留在实现PipeTransform的类中。很高兴它有所帮助!:这是另一个好办法。需要注意的是,您必须在ValidationPipe之前运行RequestConverterPipe,这意味着无法全局设置ValidationPipe。是的,这是真的-好的一点。嘿,Eol,感谢您的响应!我可能需要创建一个定制的PipeTransform类,如图所示。尽管我非常喜欢下面Jay示例中DTO中的单线转换,但我遇到的问题是,当入站属性的大小写与DTO属性的大小写不匹配时,我无法将对象和值参数放入转换中。我只是不知道是哪一层造成了这个问题。我把你的回答标记为答案,因为这似乎是最简单的前进之路。虽然DTO上的Exclude和Expose装饰器可以工作,但我觉得我在给DTO增加额外的重量,希望尽可能多地保留DTO中的逻辑。我希望在体系结构中保持清晰的分隔线,使DTO尽可能精简,以履行其作为传输实体的角色,不执行转换,并将转换操作留在实现PipeTransform的类中。很高兴它有所帮助!:为什么不坚持使用dto中定义的名称呢?你好,我不完全理解你
你的评论。你能详细说明一下吗?我的意思是,让道具名称保持在legacy consumer中,并使用相同的或添加其他getter方法,使其小写{return this.camelCase}。或者,如果需要,可以添加一个序列化程序来排除CamelCased值。或者是带有@Expose{name:lowercase}的classToClass序列化程序,为什么不坚持使用dto中定义的名称呢?您好,我不完全理解您的评论。你能详细说明一下吗?我的意思是,让道具名称保持在legacy consumer中,并使用相同的或添加其他getter方法,使其小写{return this.camelCase}。或者,如果需要,可以添加一个序列化程序来排除CamelCased值。或者一个带有@Expose{name:lowercase}的classToClass序列化程序嘿,杰,谢谢你的回复!奇怪的是,如果入站JSON负载上的属性不同于DTO的属性,则转换的对象在值或obj参数中不可用。DTO属性为propertyOne,入站JSON有效负载属性为propertyOne。如果我将inbound JSON payload属性更改为propertyOne,则转换具有适当的值和obj参数,但这会破坏purpose@BSmith你是对的,我假设在@Transform类transformer中,无论属性如何,都可以访问完整对象。我已经更新了我的答案,提供了一些有用的东西,但它不一定漂亮。使用classToPlain,plaintoClass时,不需要getter重命名。而是@Exclude{toPlainOnly:true}在任何其他道具名称上使用Expose{name:'lowercase'}。嘿,杰,谢谢你的回复!奇怪的是,如果入站JSON负载上的属性不同于DTO的属性,则转换的对象在值或obj参数中不可用。DTO属性为propertyOne,入站JSON有效负载属性为propertyOne。如果我将inbound JSON payload属性更改为propertyOne,则转换具有适当的值和obj参数,但这会破坏purpose@BSmith你是对的,我假设在@Transform类transformer中,无论属性如何,都可以访问完整对象。我已经更新了我的答案,提供了一些有用的东西,但它不一定漂亮。使用classToPlain,plaintoClass时,不需要getter重命名。而是@Exclude{toPlainOnly:true}在任何其他道具名称上使用Expose{name:'lowercase'}。