Custom validation and schema capabilities allow developers to extend class-validator with custom validation logic, create reusable validators, and use programmatic schema-based validation as an alternative to decorators. These advanced features provide flexibility for complex business rules and dynamic validation scenarios.
Create custom validation decorators using existing constraint classes or inline validation logic.
/**
* Validate using a custom constraint class
* @param constraintClass - Class implementing ValidatorConstraintInterface
* @param constraints - Array of constraint values passed to validator
* @param validationOptions - Standard validation options
*/
@Validate(constraintClass: Function, constraints?: any[], validationOptions?: ValidationOptions): PropertyDecorator;
/**
* Core decorator for building custom validation decorators
* @param options - Validation configuration with validator function
* @param validationOptions - Standard validation options
*/
@ValidateBy(options: ValidateByOptions, validationOptions?: ValidationOptions): PropertyDecorator;
/**
* Options for ValidateBy decorator
*/
interface ValidateByOptions {
/** Name of the validation */
name: string;
/** Validator function or object with validate method */
validator: {
validate(value: any, args?: ValidationArguments): boolean | Promise<boolean>;
defaultMessage?(args?: ValidationArguments): string;
};
/** Whether validation is async */
async?: boolean;
/** Constraint values */
constraints?: any[];
}Define reusable validation constraint classes for complex validation logic.
/**
* Interface for custom constraint classes
*/
interface ValidatorConstraintInterface {
/**
* Method to be called to perform custom validation over given value
* @param value - Value being validated
* @param validationArguments - Validation context and constraints
* @returns Boolean indicating validation success or Promise<boolean> for async
*/
validate(value: any, validationArguments?: ValidationArguments): boolean | Promise<boolean>;
/**
* Gets default message when validation fails
* @param validationArguments - Validation context for error message
* @returns Error message string
*/
defaultMessage?(validationArguments?: ValidationArguments): string;
}
/**
* Decorator to mark a class as a validator constraint
* @param options - Constraint configuration options
*/
@ValidatorConstraint(options?: ValidatorConstraintOptions): ClassDecorator;
interface ValidatorConstraintOptions {
/** Name of the constraint */
name?: string;
/** Whether the constraint is async */
async?: boolean;
}Programmatically register custom validation decorators.
/**
* Register a custom validation decorator
* @param options - Decorator registration options
*/
function registerDecorator(options: ValidationDecoratorOptions): void;
interface ValidationDecoratorOptions {
/** Target class constructor */
target: Function;
/** Property name to validate */
propertyName: string;
/** Validation name */
name?: string;
/** Whether validation is async */
async?: boolean;
/** Decorator options */
options?: ValidationOptions;
/** Constraint values */
constraints?: any[];
/** Validator implementation */
validator: ValidatorConstraintInterface | Function;
}Alternative validation approach using programmatic schemas instead of decorators.
/**
* Validation schema interface for programmatic validation
*/
interface ValidationSchema {
/** Schema name for registration */
name: string;
/** Property validation rules */
properties: {[propertyName: string]: ValidationSchemaProperty[]};
}
/**
* Individual property validation rule in schema
*/
interface ValidationSchemaProperty {
/** Validation type name */
type: string;
/** Validator name (optional) */
name?: string;
/** Constraint values */
constraints?: any[];
/** Error message */
message?: string | ((value?: any, constraint1?: any, constraint2?: any) => string);
/** Apply to each array element */
each?: boolean;
/** Always validate regardless of groups */
always?: boolean;
/** Validation groups */
groups?: string[];
/** Type-specific options */
options?: any;
}
/**
* Register a validation schema
* @param schema - Schema to register
*/
function registerSchema(schema: ValidationSchema): void;import { ValidateBy, ValidationArguments } from "class-validator";
class User {
@ValidateBy({
name: "isOddNumber",
validator: {
validate(value: any, args?: ValidationArguments) {
return typeof value === "number" && value % 2 === 1;
},
defaultMessage(args?: ValidationArguments) {
return `${args?.property} must be an odd number`;
}
}
})
favoriteNumber: number;
@ValidateBy({
name: "isStrongPassword",
validator: {
validate(value: any) {
if (typeof value !== "string") return false;
return value.length >= 8 &&
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/[0-9]/.test(value) &&
/[^A-Za-z0-9]/.test(value);
},
defaultMessage() {
return "Password must be at least 8 characters with uppercase, lowercase, number and special character";
}
}
})
password: string;
}
const user = new User();
user.favoriteNumber = 7; // Valid (odd number)
user.password = "MyP@ssw0rd"; // Valid (strong password)import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, Validate } from "class-validator";
@ValidatorConstraint({ name: "isLongerThan", async: false })
class IsLongerThanConstraint implements ValidatorConstraintInterface {
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;
}
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${args.property} must be longer than ${relatedPropertyName}`;
}
}
@ValidatorConstraint({ name: "isUniqueEmail", async: true })
class IsUniqueEmailConstraint implements ValidatorConstraintInterface {
async validate(value: any, args: ValidationArguments) {
// Simulate async database check
const existingEmails = ["admin@example.com", "test@example.com"];
return !existingEmails.includes(value);
}
defaultMessage(args: ValidationArguments) {
return "Email address is already registered";
}
}
class Post {
@IsString()
@IsNotEmpty()
title: string;
@IsString()
@IsNotEmpty()
@Validate(IsLongerThanConstraint, ["title"])
content: string; // Must be longer than title
}
class User {
@IsEmail()
@Validate(IsUniqueEmailConstraint)
email: string; // Must be unique email
}import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, Validate } from "class-validator";
@ValidatorConstraint({ name: "isBeforeDate", async: false })
class IsBeforeDateConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
if (!(value instanceof Date) || !(relatedValue instanceof Date)) {
return false;
}
return value < relatedValue;
}
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${args.property} must be before ${relatedPropertyName}`;
}
}
@ValidatorConstraint({ name: "passwordMatch", async: false })
class PasswordMatchConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const password = (args.object as any).password;
return value === password;
}
defaultMessage() {
return "Password confirmation must match password";
}
}
class EventBooking {
@IsDate()
startDate: Date;
@IsDate()
@Validate(IsBeforeDateConstraint, ["startDate"])
endDate: Date;
}
class UserRegistration {
@IsString()
@Length(8, 50)
password: string;
@IsString()
@Validate(PasswordMatchConstraint)
confirmPassword: string;
}import { ValidateBy } from "class-validator";
class ApiConfiguration {
@ValidateBy({
name: "isValidApiKey",
async: true,
validator: {
async validate(value: any) {
if (typeof value !== "string") return false;
// Simulate API key validation
try {
const response = await fetch(`https://api.example.com/validate?key=${value}`);
return response.ok;
} catch {
return false;
}
},
defaultMessage() {
return "Invalid API key";
}
}
})
apiKey: string;
@ValidateBy({
name: "isDomainAccessible",
async: true,
validator: {
async validate(value: any) {
if (typeof value !== "string") return false;
try {
const response = await fetch(`https://${value}`, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
},
defaultMessage() {
return "Domain is not accessible";
}
}
})
webhookDomain: string;
}import { registerSchema, validate } from "class-validator";
// Register a validation schema
registerSchema({
name: "userSchema",
properties: {
name: [
{ type: "isNotEmpty" },
{ type: "length", constraints: [2, 50] }
],
email: [
{ type: "isEmail" }
],
age: [
{ type: "isNumber" },
{ type: "min", constraints: [18] },
{ type: "max", constraints: [120] }
],
tags: [
{ type: "isArray" },
{ type: "arrayMinSize", constraints: [1] },
{ type: "isString", each: true }
]
}
});
// Use schema for validation
async function validateUserData(userData: any) {
const errors = await validate("userSchema", userData);
return errors;
}
// Example usage
const userData = {
name: "John Doe",
email: "john@example.com",
age: 25,
tags: ["developer", "javascript"]
};
const errors = await validateUserData(userData);
if (errors.length === 0) {
console.log("User data is valid");
}import { ValidationSchema, registerSchema, validate } from "class-validator";
function createDynamicSchema(config: any): ValidationSchema {
const properties: {[key: string]: any[]} = {};
Object.keys(config.fields).forEach(fieldName => {
const fieldConfig = config.fields[fieldName];
const validations = [];
if (fieldConfig.required) {
validations.push({ type: "isNotEmpty" });
}
if (fieldConfig.type === "email") {
validations.push({ type: "isEmail" });
}
if (fieldConfig.type === "number") {
validations.push({ type: "isNumber" });
if (fieldConfig.min !== undefined) {
validations.push({ type: "min", constraints: [fieldConfig.min] });
}
if (fieldConfig.max !== undefined) {
validations.push({ type: "max", constraints: [fieldConfig.max] });
}
}
properties[fieldName] = validations;
});
return {
name: config.schemaName,
properties
};
}
// Dynamic configuration
const formConfig = {
schemaName: "dynamicForm",
fields: {
title: { required: true, type: "string" },
email: { required: true, type: "email" },
score: { required: false, type: "number", min: 0, max: 100 }
}
};
const schema = createDynamicSchema(formConfig);
registerSchema(schema);import { registerDecorator, ValidationOptions, ValidationArguments } from "class-validator";
// Create reusable custom decorator
function IsDateBefore(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: "isDateBefore",
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 value instanceof Date &&
relatedValue instanceof Date &&
value < relatedValue;
},
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${args.property} must be before ${relatedPropertyName}`;
}
}
});
};
}
function IsEqualTo(property: string, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: "isEqualTo",
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 value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${args.property} must be equal to ${relatedPropertyName}`;
}
}
});
};
}
// Use custom decorators
class EventDates {
@IsDate()
startDate: Date;
@IsDate()
@IsDateBefore("startDate", { message: "End date must be after start date" })
endDate: Date;
}
class PasswordForm {
@IsString()
@Length(8, 50)
password: string;
@IsString()
@IsEqualTo("password", { message: "Passwords must match" })
confirmPassword: string;
}import { ValidateBy, ValidationArguments } from "class-validator";
class ConditionalValidation {
accountType: 'personal' | 'business';
@ValidateBy({
name: "conditionalTaxId",
validator: {
validate(value: any, args: ValidationArguments) {
const obj = args.object as ConditionalValidation;
// Only validate tax ID for business accounts
if (obj.accountType === 'business') {
return typeof value === 'string' &&
value.length > 0 &&
/^\d{2}-\d{7}$/.test(value);
}
// For personal accounts, tax ID is optional
return value === undefined || value === null || value === '';
},
defaultMessage(args: ValidationArguments) {
const obj = args.object as ConditionalValidation;
if (obj.accountType === 'business') {
return 'Business accounts must provide a valid tax ID (format: XX-XXXXXXX)';
}
return 'Tax ID validation error';
}
}
})
taxId?: string;
}@ValidatorConstraint({ name: "isUnique", async: true })
class IsUniqueConstraint implements ValidatorConstraintInterface {
async validate(value: any, args: ValidationArguments) {
const [tableName, columnName] = args.constraints;
// Simulate database query
const count = await db.count(tableName, { [columnName]: value });
return count === 0;
}
}
function IsUnique(tableName: string, columnName: string, validationOptions?: ValidationOptions) {
return Validate(IsUniqueConstraint, [tableName, columnName], validationOptions);
}@ValidatorConstraint({ name: "businessHours", async: false })
class BusinessHoursConstraint implements ValidatorConstraintInterface {
validate(value: any) {
if (!(value instanceof Date)) return false;
const hours = value.getHours();
const day = value.getDay();
// Monday-Friday, 9 AM - 5 PM
return day >= 1 && day <= 5 && hours >= 9 && hours < 17;
}
defaultMessage() {
return "Appointment must be during business hours (Mon-Fri, 9 AM - 5 PM)";
}
}function IsValidFileType(allowedTypes: string[], validationOptions?: ValidationOptions) {
return ValidateBy({
name: "isValidFileType",
validator: {
validate(value: any) {
if (!value || typeof value.mimetype !== 'string') return false;
return allowedTypes.includes(value.mimetype);
},
defaultMessage() {
return `File type must be one of: ${allowedTypes.join(', ')}`;
}
}
}, validationOptions);
}