Skip to main content

class-validator和class-transformer

· 31 min read
XOne
webmaster

用于 Typescript 或 ES6+的类验证,基于validator.js

手动验证方法列表验证装饰器列表

class-validator 中文文档

安装

npm install class-validator --save

基本使用

创建一个Post作为演示,在每个属性上添加不同的验证装饰器尝试

import { validate, validateOrReject, Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, Min, Max } from 'class-validator';

export class Post {
@Length(10, 20)
title: string;

@Contains('hello')
text: string;

@IsInt()
@Min(0)
@Max(10)
rating: number;

@IsEmail()
email: string;

@IsFQDN()
site: string;

@IsDate()
createDate: Date;
}

let post = new Post();
post.title = 'Hello'; // should not pass
post.text = 'this is a great post about hell world'; // should not pass
post.rating = 11; // should not pass
post.email = 'google.com'; // should not pass
post.site = 'googlecom'; // should not pass

// 如果验证失败不会停止运行程序
validate(post).then((errors) => {
if (errors.length > 0) {
console.log('validation failed. errors: ', errors);
} else {
console.log('validation succeed');
}
});

// 验证失败就停止运行程序
validateOrReject(post).catch((errors) => {
console.log('Promise rejected (validation failed). Errors: ', errors);
});
// 或者
async function validateOrRejectExample(input) {
try {
await validateOrReject(input);
} catch (errors) {
console.log('Caught promise rejection (validation failed). Errors: ', errors);
}
}

选项

validate函数的第二个参数是一个选项对象,尽量设置forbidNonWhitelistedtrue以避免 unkown 对象的输入验证

export interface ValidatorOptions {
skipMissingProperties?: boolean;
whitelist?: boolean;
forbidNonWhitelisted?: boolean;
groups?: string[];
dismissDefaultMessages?: boolean;
validationError?: {
target?: boolean,
value?: boolean,
};

forbidUnknownValues?: boolean;
}

验证错误

验证失败返回的错误数组是ValidationError类的对象的数组,格式如下

{
target: Object; // Object that was validated.
property: string; // Object's property that haven't pass validation.
value: any; // Value that haven't pass a validation.
constraints?: { // Constraints that failed validation with error messages.
[type: string]: string;
};
children?: ValidationError[]; // Contains all nested validation errors of the property
}

返回的格式如下

[{
target: /* post object */,
property: "title",
value: "Hello",
constraints: {
length: "$property must be longer than or equal to 10 characters"
}
}, {
target: /* post object */,
property: "text",
value: "this is a great post about hell world",
constraints: {
contains: "text must contain a hello string"
}
},
// and other errors
]

在 http 响应中我们一般不想在错误中暴露target,那么就可以如下方式禁用它

validator.validate(post, { validationError: { target: false } });

验证消息

我们可以自定义在ValidationError对象中返回的错误消息

import { MinLength, MaxLength } from 'class-validator';

export class Post {
@MinLength(10, {
message: 'Title is too short',
})
@MaxLength(50, {
message: 'Title is too long',
})
title: string;
}

消息可以接受几个参数作为变量,用字符串混合的方式放入,比如"$constraint1 characters"

import { MinLength, MaxLength } from 'class-validator';

export class Post {
@MinLength(10, {
// here, $constraint1 will be replaced with "10", and $value with actual supplied value
message: 'Title is too short. Minimal length is $constraint1 characters, but actual is $value',
})
@MaxLength(50, {
// here, $constraint1 will be replaced with "50", and $value with actual supplied value
message: 'Title is too long. Maximal length is $constraint1 characters, but actual is $value',
})
title: string;
}

能接受的变量如下

  • value - 被验证的值
  • constraints - 由指定验证类型定义的约束数组
  • targetName - 验证对象的类的名称
  • object - 被验证的对象
  • property - 被验证的属性名

当然message还可以接受一个函数的返回值,这个函数的参数为ValidationArguments类的对象,而ValidationArguments类的属性就是上面的变量列表

import { MinLength, MaxLength, ValidationArguments } from 'class-validator';

export class Post {
@MinLength(10, {
message: (args: ValidationArguments) => {
if (args.value.length === 1) {
return 'Too short, minimum length is 1 character';
} else {
return 'Too short, minimum length is ' + args.constraints[0] + ' characters';
}
},
})
title: string;
}

特殊类型

class-validator对一些经常使用的特殊类型有专门的处理方法

集合类型

验证数组,Sets,Map等集合类型需要开启each选项

验证数组

import { MinLength, MaxLength } from 'class-validator';

export class Post {
@MaxLength(20, {
each: true,
})
tags: string[];
}

验证 Sets

import { MinLength, MaxLength } from 'class-validator';

export class Post {
@MaxLength(20, {
each: true,
})
tags: Set<string>;
}

验证 Map

import { MinLength, MaxLength } from 'class-validator';

export class Post {
@MaxLength(20, {
each: true,
})
tags: Map<string, string>;
}

嵌套对象

一个验证的类中的某些属性可能是类一个的对象,比如Post类的user属性为User类,则可以使用@ValidateNested()方式来同时验证Post和嵌入的User

import { ValidateNested } from 'class-validator';

export class Post {
@ValidateNested()
user: User;
}

Promise 对象

如果待验证的属性是一个Promise对象,比如通过await关键字返回的值,则可以使用@ValidatePromise()

import { ValidatePromise, Min } from 'class-validator';

export class Post {
@Min(0)
@ValidatePromise()
userId: Promise<number>;
}

@ValidatePromise()也可以和@ValidateNested()一起使用

import { ValidateNested, ValidatePromise } from 'class-validator';

export class Post {
@ValidateNested()
@ValidatePromise()
user: Promise<User>;
}

高级主题

子类验证

如果定义一个从另一个继承的子类时,子类将自动继承父级的装饰器。如果在后代类中重新定义了属性,则装饰器将从该类和基类中继承

import {validate} from "class-validator";

class BaseContent {

@IsEmail()
email: string;

@IsString()
password: string;
}

class User extends BaseContent {

@MinLength(10)
@MaxLength(20)
name: string;

@Contains("hello")
welcome: string;

@MinLength(20)
password: string; /
}

let user = new User();

user.email = "invalid email"; // inherited property
user.password = "too short" // password wil be validated not only against IsString, but against MinLength as well
user.name = "not valid";
user.welcome = "helo";

validate(user).then(errors => {
// ...
}); // it will return errors for email, title and text properties

条件验证

当某个属性需要满足一定条件验证时可以使用(@ValidateIf)装饰器

import { ValidateIf, IsNotEmpty } from 'class-validator';

export class Post {
otherProperty: string;

@ValidateIf((o) => o.otherProperty === 'value')
@IsNotEmpty()
example: string;
}

白名单

一个被验证的类的对象可以定义在类中不存在的属性,在验证时不会产生错误。为了使只有添加了验证装饰器的属性才能被定义,你需要把whitelist设置为true,那么如果对象中定义一个类中不存在的属性就无法通过验证了。

import { validate } from 'class-validator';
// ...
validate(post, { whitelist: true });

开启白名单之后所有没有加上验证装饰器的属性被定义后都将无法通过验证,如果你想一些属性可以被定义但是又不想被验证,如果条件验证中的otherProperty属性,那么你需要在该属性上面添加一个@Allow装饰器

/**
* title可以被定义
* nonWhitelistedProperty不能被定义,否则验证失败
*/
import {validate, Allow, Min} from "class-validator";

export class Post {

@Allow()
title: string;

@Min(0)
views: number;

nonWhitelistedProperty: number;
}

let post = new Post();
post.title = 'Hello world!';
post.views = 420;

post.nonWhitelistedProperty = 69;
// 额外属性不能被添加,否则验证失败
(post as any).anotherNonWhitelistedProperty = "something";

validate(post).then(errors => {
// post.nonWhitelistedProperty is not defined
// (post as any).anotherNonWhitelistedProperty is not defined
...
});

如果你想要所有没有添加验证装饰器的属性都无法定义,则可以设置forbidNonWhitelistedtrue

这个一般不要设置,否则属性添加@Allow 会都没用了

import { validate } from 'class-validator';
// ...
validate(post, { whitelist: true, forbidNonWhitelisted: true });

添加上下文

你可以在验证装饰其中添加一个自定义的上下文对象,此对象在验证失败时被ValidationError的实例获取

import { validate } from 'class-validator';

class MyClass {
@MinLength(32, {
message: 'EIC code must be at least 32 characters',
context: {
errorCode: 1003,
developerNote: 'The validated string must contain 32 or more characters.',
},
})
eicCode: string;
}

const model = new MyClass();

validate(model).then((errors) => {
//errors[0].contexts['minLength'].errorCode === 1003
});

跳过缺失属性

有时候你需要跳过一些对象中没有设置的属性,比如更新数据模型时,与创建模型不同的是你只会更新部分值,那么这时候你就需要设置skipMissingPropertiestrue,当然可能一部分属性是你不想被跳过验证的,那么需要在这些属性上加上@IsDefined()装饰器,加了@IsDefined()装饰器的属性会忽略skipMissingProperties而必定被验证

import { validate } from 'class-validator';
// ...
validate(post, { skipMissingProperties: true });

验证组

import { validate, Min, Length } from 'class-validator';

export class User {
@Min(12, {
groups: ['registration'],
})
age: number;

@Length(2, 20, {
groups: ['registration', 'admin'],
})
name: string;
}

let user = new User();
user.age = 10;
user.name = 'Alex';

validate(user, {
groups: ['registration'],
}); // 无法通过验证

validate(user, {
groups: ['admin'],
}); // 可以通过验证

validate(user, {
groups: ['registration', 'admin'],
}); // 无法通过验证

validate(user, {
groups: undefined, // 默认模式
}); // 无法通过验证,因为没有指定group则所有属性都将被验证

validate(user, {
groups: [],
}); // 无法通过验证 (与'groups: undefined'相同)

在验证中还有一个always: true选项,如果添加了此选项,无论验证时设定的是哪种模式的groups,都将被验证

使用服务容器

你可以使用服务容器来加载验证器通过依赖注入的方式使用。以下如何将其与typedi集成的示例:

import { Container } from 'typedi';
import { useContainer, Validator } from 'class-validator';

// do this somewhere in the global application level:
useContainer(Container);
let validator = Container.get(Validator);

// now everywhere you can inject Validator class which will go from the container
// also you can inject classes using constructor injection into your custom ValidatorConstraint-s

非装饰器验证

如果你的运行环境不支持装饰器请看这里

验证普通对象

Nest.js 中使用的验证管道就是 class-validator+class-transformer 结合的方式

由于装饰器的性质,必须使用new class()语法实例化待验证的对象。如果你使用了 class-validator 装饰器定义了类,并且想要验证普通的 JS 对象(文本对象或 JSON.parse 返回),则需要将其转换为类实例 (例如,使用class-transformer或仅使用class-transformer-validator扩展可以为您完成此任务。

自定义验证

自定义规则类

你可以创建一个自定义的验证规则的类,并在规则类上添加@ValidatorConstraint装饰器。 还可以设置验证约束名称(name选项)-该名称将在ValidationError中用作“error type”。 如果您不提供约束名称,它将自动生成。

规则类必须实现ValidatorConstraintInterface接口及validate方法,该接口定义了验证逻辑。 如果验证成功,则方法返回true,否则返回false。 自定义验证器可以是异步的,如果您想在执行一些异步操作后执行验证,只需在validate方法中返回带有布尔值的promise

我们还可以定义了可选方法defaultMessage,它在属性上的装饰器未设置错误消息的情况下定义了默认错误消息。

首选我们创建一个CustomTextLength演示用的验证规则类

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'customText', async: false })
export class CustomTextLength implements ValidatorConstraintInterface {
validate(text: string, args: ValidationArguments) {
return text.length > 1 && text.length < 10; // 对于异步验证,您必须在此处返回Promise<boolean>
}

defaultMessage(args: ValidationArguments) {
// 如果验证失败,您可以在此处提供默认错误消息
return 'Text ($value) is too short or too long!';
}
}

定义好规则后我们就可以在类中使用了

import { Validate } from 'class-validator';
import { CustomTextLength } from './CustomTextLength';

class Post {
@Validate(CustomTextLength, {
message: 'Title is too short or long!',
})
title: string;
}

validate(post).then((errors) => {
// ...
});

你也可以将自定义的约束传入规则类,并通过约束来设定验证的条件

import { Validate } from 'class-validator';
import { CustomTextLength } from './CustomTextLength';

import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';

@ValidatorConstraint()
class CustomTextLength implements ValidatorConstraintInterface {
validate(text: string, validationArguments: ValidationArguments) {
return text.length > validationArguments.constraints[0] && text.length < validationArguments.constraints[1];
}
}

class Post {
@Validate(CustomTextLength, [3, 20], {
message: 'Wrong post title',
})
title: string;
}

自定义装饰器

创建自定义装饰器的方法类似创建自定义规则类,只是使用装饰器而已

装饰器的详细使用请看我这篇文章

import {registerDecorator, ValidationOptions, ValidationArguments} from "class-validator";

function IsLongerThan(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: "isLongerThan",
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return typeof value === "string" &&
typeof relatedValue === "string" &&
value.length > relatedValue.length; // you can return a Promise<boolean> here as well, if you want to make async validation
}
}
});
};
}

export class Post {

title: string;

@IsLongerThan("title", {
/* you can also use additional validation options, like "groups" in your custom validation decorators. "each" is not supported */
message: "Text must be longer than the title"
})
text: string;

}

在自定义装饰器上仍然可以使用ValidationConstraint装饰器。我们在创建一个IsUserAlreadyExist验证装饰器演示

import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ async: true })
class IsUserAlreadyExistConstraint implements ValidatorConstraintInterface {
validate(userName: any, args: ValidationArguments) {
return UserRepository.findOneByName(userName).then((user) => {
if (user) return false;
return true;
});
}
}

function IsUserAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsUserAlreadyExistConstraint,
});
};
}

class User {
@IsUserAlreadyExist({
message: 'User $value already exists. Choose another name.',
})
name: string;
}

同步验证

如果只是想简单的进行同步验证,可以使用validateSync代替validate。不过需要注意的是validateSync会忽略所有的异步验证。

class-transfomer 中文文档

类转换器的作用是将普通的 javascript 对象转换成类对象。我们通过 api 端点或者 json 文件访问所得的是普通的 json 文本,一般我们通过JSON.parse把其转换成普通的 javascript 对象,但是有时候我们想让它变成一个类的对象而不是普通的 javascript 对象。比如用class-validator来验证从后端 api 获取的 json 字符串时,我们就需要自动把 json 转为待验证类的对象而不是一个 js 对象。

例如我们现在可以读取远程 api 的一个users.json的内容如下

[
{
"id": 1,
"firstName": "Johny",
"lastName": "Cage",
"age": 27
},
{
"id": 2,
"firstName": "Ismoil",
"lastName": "Somoni",
"age": 50
},
{
"id": 3,
"firstName": "Luke",
"lastName": "Dacascos",
"age": 12
}
]

我们有一个User

export class User {
id: number;
firstName: string;
lastName: string;
age: number;

getName() {
return this.firstName + ' ' + this.lastName;
}

isAdult() {
return this.age > 36 && this.age < 60;
}
}

然后你想通过user.json来获取User的对象数组

fetch('users.json').then((users: User[]) => {
// you can use users here, and type hinting also will be available to you,
// but users are not actually instances of User class
// this means that you can't use methods of User class
});

现在你可以获取users[0].firstname但是由于你获取的是普通的 js 对象而非User类的对象,所以你无法调用users[0].getName()方法,而class-transformer就是为了把普通的 js 对象按你的需求转换成类对象而生的。

你只要像下面这样就可以创建真正的User[]对象数组了

fetch('users.json').then((users: Object[]) => {
const realUsers = plainToClass(User, users);
// now each user in realUsers is instance of User class
});

安装

  1. 安装 class-transformer: npm install class-transformer --save

  2. 安装reflect-metadata

    reflect-metadata 是必须的,具体使用请看这篇博文

安装后在app.ts这种顶层文件你需要import "reflect-metadata";

基础方法

plainToClass

普通对象转换为类对象

import { plainToClass } from 'class-transformer';

let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays

plainToClassFromExist

普通对象合并已经创建的类实例

const defaultUser = new User();
defaultUser.role = 'user';

let mixedUser = plainToClassFromExist(defaultUser, user); // mixed user should have the value role = user when no value is set otherwise.

classToPlain

类实例转换为普通对象

转换后可以使用JSON.stringify再转成普通的 json 文本

import { classToPlain } from 'class-transformer';
let photo = classToPlain(photo);

classToClass

克隆类实例

import { classToClass } from 'class-transformer';
let photo = classToClass(photo);

可以使用ignoreDecorators选项去除所有原实例中的装饰器

serialize

直接把类实例转换为 json 文本,是不是数组都可以转换

import { serialize } from 'class-transformer';
let photo = serialize(photo);

deserialize 和 deserializeArray

直接把 json 文本转换为类对象

import { deserialize } from 'class-transformer';
let photo = deserialize(Photo, photo);

如果 json 文本是个对象数组请使用deserializeArray方法

import { deserializeArray } from 'class-transformer';
let photos = deserializeArray(Photo, photos);

强制类型安全

plainToClass会把所有的被转换对象的属性全部类实例的属性,即时类中并不存在某些属性

import { plainToClass } from 'class-transformer';

class User {
id: number;
firstName: string;
lastName: string;
}

const fromPlainUser = {
unkownProp: 'hello there',
firstName: 'Umed',
lastName: 'Khudoiberdiev',
};

console.log(plainToClass(User, fromPlainUser));

// User {
// unkownProp: 'hello there',
// firstName: 'Umed',
// lastName: 'Khudoiberdiev',
// }

你可以使用excludeExtraneousValues选项结合Expose装饰器来指定需要公开的属性

import { Expose, plainToClass } from 'class-transformer';

class User {
@Expose() id: number;
@Expose() firstName: string;
@Expose() lastName: string;
}

const fromPlainUser = {
unkownProp: 'hello there',
firstName: 'Umed',
lastName: 'Khudoiberdiev',
};

console.log(plainToClass(User, fromPlainUser, { excludeExtraneousValues: true }));

// User {
// id: undefined,
// firstName: 'Umed',
// lastName: 'Khudoiberdiev'
// }

子类型转换

嵌套对象

由于现在 Typescript 对反射还没有非常好的支持,所以你需要使用@Type装饰器来隐式地指定属性所属的类

import { Type, plainToClass } from 'class-transformer';

export class Album {
id: number;

name: string;

@Type(() => Photo)
photos: Photo[];
}

export class Photo {
id: number;
filename: string;
}

let album = plainToClass(Album, albumJson);
// now album is Album object with Photo objects inside

多类型选项

一个嵌套的子类型也可以匹配多个类型,这可以通过判断器实现。判断器需要指定一个 property,而被转换 js 对象中的嵌套对象的也必须拥有与property相同的一个字段,并把值设置为需要转换的子类型的名称。判断器还需要指定所有的子类型值以及其名称,具体示例如下

import {Type, plainToClass} from "class-transformer";

const albumJson = {
"id": 1,
"name": "foo",
"topPhoto": {
"id": 9,
"filename": "cool_wale.jpg",
"depth": 1245,
"__type": "underwater"
}
}

export abstract class Photo {
id: number;
filename: string;
}

export class Landscape extends Photo {
panorama: boolean;
}

export class Portrait extends Photo {
person: Person;
}

export class UnderWater extends Photo {
depth: number;
}

export class Album {

id: number;
name: string;

@Type(() => Photo, {
discriminator: {
property: "__type",
subTypes: [
{ value: Landscape, name: "landscape" },
{ value: Portrait, name: "portrait" },
{ value: UnderWater, name: "underwater" }
]
}
})
topPhoto: Landscape | Portrait | UnderWater;

}

let album = plainToClass(Album, albumJson);
// now album is Album object with a UnderWater object without `__type` property.

此外可以设置keepDiscriminatorProperty: true,这样可以把判断器的属性也包含在转换后的对象中

排除与公开

公开方法的返回值

添加@Expose装饰器即可公开 getter 和方法的返回值

import { Expose } from 'class-transformer';

export class User {
id: number;
firstName: string;
lastName: string;
password: string;

@Expose()
get name() {
return this.firstName + ' ' + this.lastName;
}

@Expose()
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}

公开属性为不同名称

如果要使用其他名称公开某些属性,可以通过为@Expose装饰器指定name选项来实现:

import { Expose } from 'class-transformer';

export class User {
@Expose({ name: 'uid' })
id: number;

firstName: string;

lastName: string;

@Expose({ name: 'secretKey' })
password: string;

@Expose({ name: 'fullName' })
getFullName() {
return this.firstName + ' ' + this.lastName;
}
}

跳过指定属性

有时您想在转换过程中跳过一些属性。这可以使用@Exclude装饰器完成:

import { Exclude } from 'class-transformer';

export class User {
id: number;

email: string;

@Exclude()
password: string;
}

现在,当您转换用户时,password属性将被跳过,并且不包含在转换结果中。

根据操作决定跳过

我们可以通过toClassOnly或者toPlainOnly来控制一个属性在哪些操作中需要排除

import { Exclude } from 'class-transformer';

export class User {
id: number;

email: string;

@Exclude({ toPlainOnly: true })
password: string;
}

现在password属性将会在classToPlain操作中排除,相反的可以使用toClassOnly

跳过类的所有属性

你可以通过在类上添加@Exclude装饰器并且在需要公开的属性上添加@Expose装饰器来只公开指定的属性

import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class User {
@Expose()
id: number;

@Expose()
email: string;

password: string;
}

另外,您可以在转换期间设置排除策略:

import { classToPlain } from 'class-transformer';
let photo = classToPlain(photo, { strategy: 'excludeAll' });

这时你不需要在添加@Exclude装饰器了

跳过私有属性或某些前缀属性

我们可以排除公开具有指定前缀的属性以及私有属性

import {Expose} from "class-transformer";

export class User {

id: number;
private _firstName: string;
private _lastName: string;
_password: string;

setName(firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}

@Expose()
get name() {
return this.firstName + " " + this.lastName;
}

}

const user = new User();
user.id = 1;
user.setName("Johny", "Cage");
user._password = 123;

const plainUser = classToPlain(user, { excludePrefixes: ["_"] });
// here plainUser will be equal to
// { id: 1, name: "Johny Cage" }

使用组来控制排除的属性

import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class User {
id: number;

name: string;

@Expose({ groups: ['user', 'admin'] }) // this means that this data will be exposed only to users and admins
email: string;

@Expose({ groups: ['user'] }) // this means that this data will be exposed only to users
password: string;
}

let user1 = classToPlain(user, { groups: ['user'] }); // will contain id, name, email and password
let user2 = classToPlain(user, { groups: ['admin'] }); // will contain id, name and email

使用版本范围来控制公开和排除的属性

如果要构建具有不同版本的 API,则 class-transformer 具有非常有用的工具。您可以控制应在哪个版本中公开或排除模型的哪些属性。示例

import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class User {
id: number;

name: string;

@Expose({ since: 0.7, until: 1 }) // this means that this property will be exposed for version starting from 0.7 until 1
email: string;

@Expose({ since: 2.1 }) // this means that this property will be exposed for version starting from 2.1
password: string;
}

let user1 = classToPlain(user, { version: 0.5 }); // will contain id and name
let user2 = classToPlain(user, { version: 0.7 }); // will contain id, name and email
let user3 = classToPlain(user, { version: 1 }); // will contain id and name
let user4 = classToPlain(user, { version: 2 }); // will contain id and name
let user5 = classToPlain(user, { version: 2.1 }); // will contain id, name nad password

特殊处理

将日期字符串转换为 Date 对象

有时,您的 JavaScript 对象中有一个以字符串格式接收的 Date。您想从中创建一个真正的 javascript Date 对象。您只需将 Date 对象传递给@Type装饰器即可完成此操作:

当从类对象反向转换为普通对象时 registrationDate 将会被转回为字符串

import { Type } from 'class-transformer';

export class User {
id: number;

email: string;

password: string;

@Type(() => Date)
registrationDate: Date;
}

当您想将值转换为Number, String, Boolean 类型时也是这样做

数组处理

当你想转换数组时,你必须使用@Type装饰器指定数组项的类型也可以使用自定义的数组类型

SetMap也是一样

import { Type } from 'class-transformer';

export class AlbumCollection extends Array<Album> {
// custom array functions ...
}

export class Photo {
id: number;

name: string;

@Type(() => Album)
albums: Album[];
// albums: AlbumCollection; 使用自定义类型
}

export class Skill {
name: string;
}

export class Weapon {
name: string;
range: number;
}

export class Player {
name: string;

@Type(() => Skill)
skills: Set<Skill>;

@Type(() => Weapon)
weapons: Map<string, Weapon>;
}

自定义转换

基本使用

你可以使用@Transform添加额外的数据转换,例如当你想把通过普通对象中的字符串日期转换后的date对象继续转换变成moment库的对象:

import { Transform } from 'class-transformer';
import * as moment from 'moment';
import { Moment } from 'moment';

export class Photo {
id: number;

@Type(() => Date)
@Transform((value) => moment(value), { toClassOnly: true })
date: Moment;
}

现在当执行plainToClass转换后的对象中的date属性将是一个Moment对象。@Transform同样支持组和版本。

高级用法

@Transform有更多的参数给你创建自定义的转换逻辑

@Transform((value, obj, type) => value)
参数描述
value自定义转换执行前的属性值
obj转换源对象
type转换的类型

其他装饰器

签名示例
@TransformClassToPlain@TransformClassToPlain({ groups: ["user"] })
@TransformClassToClass@TransformClassToClass({ groups: ["user"] })
@TransformPlainToClas@TransformPlainToClass(User, { groups: ["user"] })

上述装饰器接受一个可选参数:ClassTransformOptions-转换选项,例如 groups, version, name,示例:

@Exclude()
class User {
id: number;

@Expose()
firstName: string;

@Expose()
lastName: string;

@Expose({ groups: ['user.email'] })
email: string;

password: string;
}

class UserController {
@TransformClassToPlain({ groups: ['user.email'] })
getUser() {
const user = new User();
user.firstName = 'Snir';
user.lastName = 'Segal';
user.password = 'imnosuperman';

return user;
}
}

const controller = new UserController();
const user = controller.getUser();

user对象将包含 firstname,latstname 和 email

使用泛型

由于目前 Typescript 对反射的支持还没有完善,所以只能使用其它替代方案,具体可以查看这个例子

隐式类型转换

你如果将 class-validator 与 class-transformer 一起使用,则可能不想启用此功能。

根据 Typescript 提供的类型信息,启用内置类型之间的自动转换。默认禁用。

import { IsString } from 'class-validator';

class MyPayload {
@IsString()
prop: string;
}

const result1 = plainToClass(MyPayload, { prop: 1234 }, { enableImplicitConversion: true });
const result2 = plainToClass(MyPayload, { prop: 1234 }, { enableImplicitConversion: false });

/**
* result1 will be `{ prop: "1234" }` - notice how the prop value has been converted to string.
* result2 will be `{ prop: 1234 }` - default behaviour
*/

循环引用

如果User包含一个Photo类型的photos数组属性,而Photo又包含一个属性链接到User,则转换过程中此属性会被忽略,除了classToClass操作。