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

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

安装

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会忽略所有的异步验证。

Last modification:February 24th, 2020 at 08:51 pm
原创不易,请赞助一包烟钱,让lichnow能更好地服务您^v^