Skip to content

Custom Validation

Custom Validation Classes

Creating a Custom Validator

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;
    }

    defaultMessage(args: ValidationArguments) {
        return 'Text ($value) is too short or too long!';
    }
}

We marked our class with the @ValidatorConstraint decorator. You can also supply a validation constraint name — this name will be used as the "error type" in ValidationError. If you don't supply a constraint name, it will be auto-generated.

Our class must implement the ValidatorConstraintInterface interface and its validate method, which defines the validation logic. If validation succeeds, the method returns true, otherwise false. Custom validators can be asynchronous — if you want to perform validation after some asynchronous operation, simply return a Promise<boolean> from validate.

The optional defaultMessage method defines a default error message, used when the decorator's implementation doesn't set its own error message.

Using Custom Validator

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

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

You can also pass constraints to your validator:

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

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

And use them from the validationArguments object:

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

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

Custom Validation Decorators

You can also create a custom decorator. It's the most elegant way of using custom validations. Let's create a decorator called @IsLongerThan:

Creating a Custom Decorator

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

export 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;
                }
            }
        });
    };
}

Using Custom Decorator

import { IsLongerThan } from './IsLongerThan';

export class Post {
    title: string;

    @IsLongerThan('title', {
        message: 'Text must be longer than the title'
    })
    text: string;
}

In your custom decorators you can also use ValidatorConstraint. Let's create another custom validation decorator called IsUserAlreadyExist:

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

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

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

Note that we marked our constraint as async by adding { async: true } to the validation options. Then put it to use:

import { IsUserAlreadyExist } from './IsUserAlreadyExist';

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

Need documentation like this for your own product?

This site was built by Sonicar Tech LLC — we help SaaS, B2B, Enterprise, FinTech, and AI companies launch professional, docs-as-code documentation 60% faster and cheaper than building an in-house team, with first drafts delivered in 1 week.

Visit sonicar.tech