Использование функций для динамических сообщений

В библиотеке class-validator сообщения об ошибках не обязаны быть статичными строками. Во многих практических сценариях требуется формировать текст ошибки на основе состояния объекта, параметров декоратора или значения самого поля. Для этого используются функции, возвращающие строку сообщения.

Базовый принцип заключается в том, что вместо строки в свойство message передаётся функция:

message: (validationArguments) => string

Эта функция получает объект ValidationArguments, содержащий контекст текущей валидации, и возвращает итоговое сообщение.


Контекст ValidationArguments

Объект ValidationArguments является ключевым элементом при построении динамических сообщений. Он содержит:

  • value — текущее значение поля
  • constraints — массив параметров, переданных в декоратор
  • targetName — имя класса
  • object — экземпляр объекта, проходящего валидацию
  • property — имя проверяемого свойства

Пример структуры:

interface ValidationArguments {
  value: any;
  constraints: any[];
  targetName: string;
  object: any;
  property: string;
}

Эти данные позволяют формировать сообщения, учитывающие не только значение поля, но и бизнес-логику объекта.


Простая динамика на основе значения

Наиболее частый сценарий — включение фактического значения поля в текст ошибки.

import { IsNotEmpty } from 'class-validator';

class User {
  @IsNotEmpty({
    message: (args) => `Поле "${args.property}" не должно быть пустым. Получено: ${args.value}`,
  })
  name: string;
}

Здесь сообщение формируется динамически:

  • используется имя поля через args.property
  • отображается текущее значение через args.value

Это упрощает отладку и делает ошибки более информативными.


Использование constraints для параметризации сообщений

Многие валидаторы принимают дополнительные параметры. Эти параметры становятся доступны в args.constraints.

Пример с ограничением длины:

import { Length } from 'class-validator';

class Product {
  @Length(5, 20, {
    message: (args) => {
      const [min, max] = args.constraints;
      return `Название должно содержать от ${min} до ${max} символов. Текущее значение: "${args.value}"`;
    },
  })
  title: string;
}

Механизм constraints позволяет делать декораторы универсальными и переиспользуемыми, не хардкодя значения в тексте ошибок.


Условные сообщения на основе объекта

Валидация часто зависит не только от одного поля, но и от состояния всего объекта.

import { Min } from 'class-validator';

class Order {
  status: string;

  @Min(100, {
    message: (args) => {
      const order = args.object as Order;

      if (order.status === 'VIP') {
        return `Для VIP-заказов минимальная сумма выше. Текущее значение: ${args.value}`;
      }

      return `Минимальная сумма заказа — ${args.constraints[0]}`;
    },
  })
  amount: number;
}

Здесь используется:

  • args.object для доступа ко всему объекту
  • условная логика для изменения текста ошибки

Такой подход позволяет адаптировать сообщения под бизнес-правила без написания отдельных валидаторов.


Формирование сообщений через внешние функции

Для крупных проектов встроенные функции в декораторах быстро становятся громоздкими. Логичнее выносить генерацию сообщений в отдельные функции.

function lengthMessage(args: ValidationArguments): string {
  const [min, max] = args.constraints;
  return `${args.property}: допустимая длина ${min}-${max}, получено ${args.value}`;
}

class Profile {
  @Length(3, 15, {
    message: lengthMessage,
  })
  username: string;
}

Такой подход дает:

  • повторное использование логики
  • упрощение тестирования
  • чистоту моделей

Унификация сообщений через фабрики

Когда требуется стандартизировать сообщения по всему проекту, используется фабрика генераторов.

function createLengthMessage(prefix: string) {
  return (args: ValidationArguments) => {
    const [min, max] = args.constraints;
    return `${prefix}: поле "${args.property}" должно быть от ${min} до ${max} символов`;
  };
}

class Account {
  @Length(6, 30, {
    message: createLengthMessage('Ошибка валидации аккаунта'),
  })
  login: string;
}

Фабрика позволяет:

  • задавать контекст (модуль, домен, тип сущности)
  • стандартизировать стиль ошибок
  • уменьшать дублирование

Динамика на основе локализации

Одним из частых применений функций сообщений является интернационализация.

const messages = {
  ru: {
    required: (field: string) => `Поле ${field} обязательно`,
    length: (min: number, max: number) =>
      `Длина должна быть от ${min} до ${max} символов`,
  },
  en: {
    required: (field: string) => `Field ${field} is required`,
    length: (min: number, max: number) =>
      `Length must be between ${min} and ${max} characters`,
  },
};

const lang = 'ru';

class Customer {
  @IsNotEmpty({
    message: (args) => messages[lang].required(args.property),
  })
  firstName: string;
}

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


Использование сложной бизнес-логики

Динамические сообщения могут учитывать несколько полей и внешние правила.

class Booking {
  type: string;

  @Min(1, {
    message: (args) => {
      const booking = args.object as Booking;

      if (booking.type === 'premium') {
        return 'Для премиум-бронирования минимальное количество — 5';
      }

      return 'Минимальное значение должно быть не меньше 1';
    },
  })
  seats: number;
}

Здесь функция сообщения фактически отражает часть бизнес-логики, но без необходимости создавать кастомный валидатор.


Ограничения подхода

Использование функций для сообщений увеличивает гибкость, но создаёт определённые нюансы:

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

Поэтому динамические сообщения целесообразно применять для:

  • отображения значений
  • подстановки параметров декоратора
  • локализации
  • простых условных ветвлений

Для сложной валидации лучше использовать кастомные валидаторы через ValidatorConstraint.


Связь с кастомными валидаторами

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

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

@ValidatorConstraint({ name: 'isPositive', async: false })
class IsPositiveConstraint implements ValidatorConstraintInterface {
  validate(value: number) {
    return value > 0;
  }

  defaultMessage(args: ValidationArguments) {
    return `Значение ${args.property} должно быть положительным. Получено: ${args.value}`;
  }
}

Метод defaultMessage по сути является специализированной формой функции message, но применяется внутри constraint-класса.


Работа с типами и безопасностью

При использовании TypeScript важно явно приводить типы, особенно при работе с constraints:

message: (args: ValidationArguments) => {
  const [min, max] = args.constraints as [number, number];
  return `Допустимый диапазон: ${min}-${max}`;
};

Без явного приведения типы теряются, так как constraints имеет тип any[].


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

Иногда сообщение формируется из нескольких уровней данных:

class Discount {
  type: string;

  @Min(10, {
    message: (args) => {
      const discount = args.object as Discount;
      const base = args.constraints[0];

      return `Тип ${discount.type}: минимальное значение скидки ${base}%`;
    },
  })
  percent: number;
}

Такой подход позволяет объединять:

  • значение декоратора
  • состояние объекта
  • имя поля

Структурирование сложных сообщений

При росте проекта полезно стандартизировать формат сообщений:

function formatError({
  field,
  rule,
  expected,
  received,
}: {
  field: string;
  rule: string;
  expected: string | number;
  received: any;
}) {
  return `[${rule}] Поле "${field}" должно быть ${expected}, получено ${received}`;
}

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

message: (args) =>
  formatError({
    field: args.property,
    rule: 'MIN_LENGTH',
    expected: args.constraints[0],
    received: args.value,
  });

Такой слой абстракции позволяет унифицировать вывод ошибок независимо от валидатора.


Поведение при асинхронной валидации

Функции сообщений всегда остаются синхронными, даже если сама валидация асинхронная. Это важно учитывать при интеграции с внешними сервисами.

message: (args) => {
  return `Ошибка проверки значения ${args.value}`;
}

Никакие await внутри message не поддерживаются, поэтому любые внешние данные должны быть заранее загружены в объект.


Итоговая модель использования динамических сообщений

Функциональный подход к сообщениям в class-validator строится вокруг нескольких принципов:

  • использование ValidationArguments как основного источника контекста
  • применение constraints для параметризации
  • доступ к объекту через args.object
  • вынос логики сообщений в отдельные функции
  • ограничение сложности внутри декоратора

Эта модель позволяет превращать сообщения об ошибках из статических строк в контекстно-зависимый слой, тесно связанный с данными и бизнес-правилами приложения.