Примеры реальных кастомных валидаторов

Во многих системах стандартных декораторов недостаточно для описания требований к паролю. Часто требуется проверка сразу нескольких условий: длина, наличие цифр, символов верхнего и нижнего регистра, специальных символов и отсутствие пробелов.

Реализация кастомного валидатора через ValidatorConstraint позволяет вынести такую логику в переиспользуемую сущность.

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

@ValidatorConstraint({ name: 'IsStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
  validate(value, args) {
    if (typeof value !== 'string') return false;

    const lengthOk = value.length >= 8;
    const hasUpperCase = /[A-Z]/.test(value);
    const hasLowerCase = /[a-z]/.test(value);
    const hasNumber = /\d/.test(value);
    const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
    const hasSpaces = /\s/.test(value);

    return lengthOk && hasUpperCase && hasLowerCase && hasNumber && hasSpecial && !hasSpaces;
  }

  defaultMessage(args) {
    return 'Пароль не соответствует требованиям сложности';
  }
}

export function IsStrongPassword(validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsStrongPasswordConstraint,
    });
  };
}

Использование:

import { IsStrongPassword } from './validators/password.validator';

class UserDto {
  @IsStrongPassword()
  password;
}

Асинхронная проверка уникальности значения в базе данных

Реальные приложения требуют проверки уникальности email или логина. Такая логика всегда асинхронна и опирается на внешний источник данных.

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

@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async validate(email) {
    if (typeof email !== 'string') return false;

    const user = await this.userRepository.findOne({ email });
    return !user;
  }

  defaultMessage() {
    return 'Email уже используется';
  }
}

export function IsEmailUnique(validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsEmailUniqueConstraint,
    });
  };
}

Использование в DTO:

class RegisterDto {
  @IsEmailUnique()
  email;
}

Кросс-полевой валидатор (сравнение двух полей)

Распространённый сценарий — проверка совпадения пароля и подтверждения пароля.

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

@ValidatorConstraint({ name: 'MatchFields', async: false })
export class MatchFieldsConstraint implements ValidatorConstraintInterface {
  validate(value, args) {
    const [relatedPropertyName] = args.constraints;
    const relatedValue = args.object[relatedPropertyName];

    return value === relatedValue;
  }

  defaultMessage(args) {
    const [relatedPropertyName] = args.constraints;
    return `${args.property} должно совпадать с ${relatedPropertyName}`;
  }
}

export function Match(property, validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      constraints: [property],
      options: validationOptions,
      validator: MatchFieldsConstraint,
    });
  };
}

Использование:

class ChangePasswordDto {
  password;
  @Match('password')
  confirmPassword;
}

Валидатор диапазона дат с бизнес-логикой

В реальных системах часто требуется ограничение даты, например: дата бронирования не может быть в прошлом и не может превышать определённый горизонт.

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

@ValidatorConstraint({ async: false })
export class IsValidBookingDateConstraint implements ValidatorConstraintInterface {
  validate(value) {
    if (!(value instanceof Date)) return false;

    const now = new Date();
    const maxDate = new Date();
    maxDate.setMonth(maxDate.getMonth() + 6);

    return value >= now && value <= maxDate;
  }

  defaultMessage() {
    return 'Дата бронирования должна быть в пределах 6 месяцев от текущей даты';
  }
}

export function IsValidBookingDate(validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: IsValidBookingDateConstraint,
    });
  };
}

Условный валидатор на основе других полей объекта

Сложные DTO часто содержат зависимые поля: например, поле companyName обязательно только если isCompany установлено в true.

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

@ValidatorConstraint({ async: false })
export class IsRequiredIfConstraint implements ValidatorConstraintInterface {
  validate(value, args) {
    const [dependentProperty, expectedValue] = args.constraints;
    const object = args.object;

    if (object[dependentProperty] === expectedValue) {
      return value !== undefined && value !== null && value !== '';
    }

    return true;
  }

  defaultMessage(args) {
    const [dependentProperty, expectedValue] = args.constraints;
    return `${args.property} обязательно, когда ${dependentProperty} равно ${expectedValue}`;
  }
}

export function IsRequiredIf(property, value, validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      constraints: [property, value],
      options: validationOptions,
      validator: IsRequiredIfConstraint,
    });
  };
}

Пример применения:

class CompanyDto {
  isCompany;
  @IsRequiredIf('isCompany', true)
  companyName;
}

Валидатор структуры массива объектов

Валидация массивов сложных объектов часто выходит за рамки стандартных декораторов.

Пример: проверка, что каждый элемент массива имеет уникальный идентификатор.

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

@ValidatorConstraint({ async: false })
export class IsUniqueArrayFieldConstraint implements ValidatorConstraintInterface {
  validate(array) {
    if (!Array.isArray(array)) return false;

    const ids = array.map(item => item.id);
    const uniqueIds = new Set(ids);

    return ids.length === uniqueIds.size;
  }

  defaultMessage() {
    return 'Элементы массива должны иметь уникальные id';
  }
}

export function IsUniqueArrayField(validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: IsUniqueArrayFieldConstraint,
    });
  };
}

Валидация формата внешних идентификаторов (например, SKU)

В реальных системах часто используются строгие форматы артикулов или идентификаторов.

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

@ValidatorConstraint({ async: false })
export class IsSkuFormatConstraint implements ValidatorConstraintInterface {
  validate(value) {
    if (typeof value !== 'string') return false;

    return /^[A-Z]{3}-\d{4}-[A-Z0-9]{2}$/.test(value);
  }

  defaultMessage() {
    return 'SKU должен соответствовать формату XXX-0000-XX';
  }
}

export function IsSkuFormat(validationOptions) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: IsSkuFormatConstraint,
    });
  };
}

Интеграция кастомных валидаторов в архитектуру DTO

Кастомные валидаторы в class-validator становятся частью доменной логики, а не просто инструментом проверки входных данных. Их использование особенно эффективно при следующих условиях:

  • наличие бизнес-правил, выходящих за пределы простых регулярных выражений
  • необходимость переиспользования логики между DTO
  • интеграция с внешними сервисами и базами данных
  • зависимость одного поля от другого
  • сложные структуры данных (массивы, вложенные объекты)

Такая организация позволяет удерживать валидационную логику рядом с моделью данных, не перегружая сервисный слой лишними проверками и условиями.