or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

array-object-validation.mdcommon-decorators.mdcore-validation.mdcustom-validation-schema.mdindex.mdnumber-date-validation.mdstring-validation.mdtype-validation.md
tile.json

custom-validation-schema.mddocs/

Custom Validation and Schema System

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.

Capabilities

Custom Validation Decorators

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

Custom Constraint Classes

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

Decorator Registration

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

Schema-Based Validation

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;

Usage Examples

Simple Custom Validation with ValidateBy

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)

Custom Constraint Classes

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
}

Cross-Property Validation

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

Async Custom Validation

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

Schema-Based Validation

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

Dynamic Schema Generation

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

Building Reusable Custom Decorators

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

Conditional Custom Validation

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

Common Custom Validation Patterns

Database Uniqueness Validation

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

Business Rule Validation

@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)";
  }
}

File Type Validation

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