在 DTO 创建期间执行验证之前,NestJS 使用 ValidationPipe 转换 属性
NestJS transform a property using ValidationPipe before validation execution during DTO creation
我正在使用内置的 NestJS ValidationPipe 以及 class-validator 和 class-transformer 来验证和清理入站 JSON 主体有效载荷。我面临的一种情况是入站 JSON 对象中混合了大小写 属性 名称。我想纠正这些属性并将其映射到我们新的 TypeScript NestJS API 中的标准驼峰式模型,这样我就不会将遗留系统中的不匹配模式耦合到我们的新 API 和新标准中,本质上是使用 DTO 中的 @Transform 作为应用程序其余部分的隔离机制。例如,入站 JSON 对象的属性:
"propertyone",
"PROPERTYTWO",
"PropertyThree"
应该映射到
"propertyOne",
"propertyTwo",
"propertyThree"
我想使用@Transform 来完成此操作,但我认为我的方法不正确。我想知道是否需要编写自定义 ValidationPipe。这是我目前的做法。
控制器:
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;
}
}
测试模型:
import { IsNotEmpty } from 'class-validator';
export class TestMeModel {
@IsNotEmpty()
someTestProperty!: string;
}
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
}
用于 POST 控制器的示例负载:
{
"propertyone": "test1",
"PROPERTYTWO": "test2",
"PropertyThree": "test3",
"simpleModel": { "sometestproperty": "test4" }
}
我遇到的问题:
- 转换似乎没有效果。 Class 验证器告诉我这些属性中的每一个都不能为空。例如,如果我将“属性one”更改为“属性One”,那么 class 验证器验证就可以 属性,例如它看到了价值。其他两个属性也一样。如果我驼峰式排列它们,那么 class 验证器会很高兴。这是验证发生之前转换的症状而不是 运行 吗?
- 这个很奇怪。当我调试和评估 TestMeRequestDto 对象时,我可以看到 simpleModel 属性 包含一个包含 属性 名称“sometest属性”的对象,即使 Class 定义为TestMeModel 有一个驼峰命名的“someTestProperty”。为什么 @Type(() => TestMeModel) 不遵守 属性 名称的正确大小写? “test4”的值存在于此 属性 中,因此它知道如何理解该值并分配它。
- 仍然很奇怪,TestMeModel 上“someTestProperty”属性 的 @IsNotEmpty() 验证没有失败,例如它看到“test4”值并感到满意,即使样本 JSON 有效负载中的入站 属性 名称是“sometest属性”,全是小写。
我们将不胜感激来自社区的任何见解和指导。谢谢!
您可能需要使用 class-transformer 文档的 Advanced Usage 部分。本质上,您的 @Transform()
需要看起来 某些东西 像这样:
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { TestMeModel } from './testme.model';
export class TestMeRequestDto {
@IsNotEmpty()
@Transform((value, obj) => obj.propertyone.valueOf())
propertyOne!: string;
@IsNotEmpty()
@Transform((value, obj) => obj.PROPERTYTWO.valueOf())
propertyTwo!: string;
@IsNotEmpty()
@Transform((value, obj) => obj.PropertyThree.valueOf())
propertyThree!: string;
@ValidateNested({ each: true })
@Type(() => TestMeModel)
simpleModel!: TestMeModel
}
这应该采用
的传入有效负载
{
"propertyone": "value1",
"PROPERTYTWO": "value2",
"PropertyThree": "value3",
}
然后把它变成你想象中的 DTO。
编辑 2020 年 12 月 30 日
所以我最初使用 @Transform()
的想法并不像预想的那样有效,这真是令人失望,因为它看起来非常好。所以你可以做的不是那么干,但它仍然适用于 class-transformer,这是一个胜利。通过使用 @Exclude()
和 @Expose()
,您可以使用 属性 访问器作为怪异的 属性 的别名,看起来像这样:
class CorrectedDTO {
@Expose()
get propertyOne() {
return this.propertyONE;
}
@Expose()
get propertyTwo(): string {
return this.PROPERTYTWO;
}
@Expose()
get propertyThree(): string {
return this.PrOpErTyThReE;
}
@Exclude({ toPlainOnly: true })
propertyONE: string;
@Exclude({ toPlainOnly: true })
PROPERTYTWO: string;
@Exclude({ toPlainOnly: true })
PrOpErTyThReE: string;
}
现在您可以访问 dto.propertyOne
并获得预期的 属性,当您执行 classToPlain
时,它将删除 propertyONE
和其他属性(如果你使用的是 Nest 的序列化拦截器。否则在辅助管道中你可以 plainToClass(NewDTO, classToPlain(value))
其中 NewDTO
只有更正的字段)。
您可能想要研究的另一件事是 automapper 并查看它是否具有更好的类似功能。
作为 Jay 的出色回答的替代方法,您还可以创建一个自定义管道,在其中将 mapping/transforming 请求负载的逻辑保存到所需的 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> {
// ...
}
我正在使用内置的 NestJS ValidationPipe 以及 class-validator 和 class-transformer 来验证和清理入站 JSON 主体有效载荷。我面临的一种情况是入站 JSON 对象中混合了大小写 属性 名称。我想纠正这些属性并将其映射到我们新的 TypeScript NestJS API 中的标准驼峰式模型,这样我就不会将遗留系统中的不匹配模式耦合到我们的新 API 和新标准中,本质上是使用 DTO 中的 @Transform 作为应用程序其余部分的隔离机制。例如,入站 JSON 对象的属性:
"propertyone",
"PROPERTYTWO",
"PropertyThree"
应该映射到
"propertyOne",
"propertyTwo",
"propertyThree"
我想使用@Transform 来完成此操作,但我认为我的方法不正确。我想知道是否需要编写自定义 ValidationPipe。这是我目前的做法。
控制器:
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;
}
}
测试模型:
import { IsNotEmpty } from 'class-validator';
export class TestMeModel {
@IsNotEmpty()
someTestProperty!: string;
}
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
}
用于 POST 控制器的示例负载:
{
"propertyone": "test1",
"PROPERTYTWO": "test2",
"PropertyThree": "test3",
"simpleModel": { "sometestproperty": "test4" }
}
我遇到的问题:
- 转换似乎没有效果。 Class 验证器告诉我这些属性中的每一个都不能为空。例如,如果我将“属性one”更改为“属性One”,那么 class 验证器验证就可以 属性,例如它看到了价值。其他两个属性也一样。如果我驼峰式排列它们,那么 class 验证器会很高兴。这是验证发生之前转换的症状而不是 运行 吗?
- 这个很奇怪。当我调试和评估 TestMeRequestDto 对象时,我可以看到 simpleModel 属性 包含一个包含 属性 名称“sometest属性”的对象,即使 Class 定义为TestMeModel 有一个驼峰命名的“someTestProperty”。为什么 @Type(() => TestMeModel) 不遵守 属性 名称的正确大小写? “test4”的值存在于此 属性 中,因此它知道如何理解该值并分配它。
- 仍然很奇怪,TestMeModel 上“someTestProperty”属性 的 @IsNotEmpty() 验证没有失败,例如它看到“test4”值并感到满意,即使样本 JSON 有效负载中的入站 属性 名称是“sometest属性”,全是小写。
我们将不胜感激来自社区的任何见解和指导。谢谢!
您可能需要使用 class-transformer 文档的 Advanced Usage 部分。本质上,您的 @Transform()
需要看起来 某些东西 像这样:
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { TestMeModel } from './testme.model';
export class TestMeRequestDto {
@IsNotEmpty()
@Transform((value, obj) => obj.propertyone.valueOf())
propertyOne!: string;
@IsNotEmpty()
@Transform((value, obj) => obj.PROPERTYTWO.valueOf())
propertyTwo!: string;
@IsNotEmpty()
@Transform((value, obj) => obj.PropertyThree.valueOf())
propertyThree!: string;
@ValidateNested({ each: true })
@Type(() => TestMeModel)
simpleModel!: TestMeModel
}
这应该采用
的传入有效负载{
"propertyone": "value1",
"PROPERTYTWO": "value2",
"PropertyThree": "value3",
}
然后把它变成你想象中的 DTO。
编辑 2020 年 12 月 30 日
所以我最初使用 @Transform()
的想法并不像预想的那样有效,这真是令人失望,因为它看起来非常好。所以你可以做的不是那么干,但它仍然适用于 class-transformer,这是一个胜利。通过使用 @Exclude()
和 @Expose()
,您可以使用 属性 访问器作为怪异的 属性 的别名,看起来像这样:
class CorrectedDTO {
@Expose()
get propertyOne() {
return this.propertyONE;
}
@Expose()
get propertyTwo(): string {
return this.PROPERTYTWO;
}
@Expose()
get propertyThree(): string {
return this.PrOpErTyThReE;
}
@Exclude({ toPlainOnly: true })
propertyONE: string;
@Exclude({ toPlainOnly: true })
PROPERTYTWO: string;
@Exclude({ toPlainOnly: true })
PrOpErTyThReE: string;
}
现在您可以访问 dto.propertyOne
并获得预期的 属性,当您执行 classToPlain
时,它将删除 propertyONE
和其他属性(如果你使用的是 Nest 的序列化拦截器。否则在辅助管道中你可以 plainToClass(NewDTO, classToPlain(value))
其中 NewDTO
只有更正的字段)。
您可能想要研究的另一件事是 automapper 并查看它是否具有更好的类似功能。
作为 Jay 的出色回答的替代方法,您还可以创建一个自定义管道,在其中将 mapping/transforming 请求负载的逻辑保存到所需的 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> {
// ...
}