Форматирование сообщений об ошибках

Формирование сообщений об ошибках в class-validator строится вокруг механизма ValidationError, который возвращается после выполнения валидации. Каждый такой объект содержит структурированную информацию о нарушениях, а не просто строку с текстом ошибки. Это позволяет гибко преобразовывать ошибки в любой формат: от простого текста до сложных локализованных сообщений и структурированных API-ответов.

Каждая ошибка валидации представляет собой объект следующего вида:

  • property — имя свойства, которое не прошло валидацию
  • value — значение, которое было проверено
  • constraints — набор нарушенных правил
  • children — вложенные ошибки (для объектов и массивов)
  • target — исходный объект

Ключевым полем для формирования сообщений является constraints. Именно оно содержит текстовые сообщения, связанные с конкретными валидаторами.

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

{
  property: "email",
  value: "wrong-email",
  constraints: {
    isEmail: "email must be an email"
  }
}

Базовый механизм формирования сообщений

Каждый встроенный декоратор может возвращать сообщение по умолчанию. Например:

import { IsEmail } from "class-validator";

class User {
  @IsEmail()
  email;
}

При ошибке будет возвращено сообщение из стандартного набора правил. Однако этот текст можно переопределить через параметры декоратора.

Переопределение сообщений через строку

Наиболее простой способ кастомизации:

import { IsEmail } from "class-validator";

class User {
  @IsEmail({}, { message: "Некорректный формат email" })
  email;
}

В этом случае значение constraints.isEmail заменяется на заданную строку.

Такой подход применяется, когда:

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

Динамические сообщения через функцию

Более гибкий механизм — использование функции в message.

Функция получает объект контекста:

  • property
  • value
  • constraints
  • target

Пример:

import { MinLength } from "class-validator";

class User {
  @MinLength(8, {
    message: (args) =>
      `Поле ${args.property} слишком короткое. Получено: ${args.value}`
  })
  password;
}

Здесь сообщение формируется динамически на основе входных данных.

Использование constraints внутри сообщений

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

Пример с минимальной длиной:

import { MinLength } from "class-validator";

class User {
  @MinLength(8, {
    message: (args) =>
      `${args.property} должен содержать минимум ${args.constraints[0]} символов`
  })
  password;
}

args.constraints[0] содержит значение 8.

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

При масштабных проектах применяется централизованная генерация сообщений.

const buildMessage = (field, rule, value) =>
  `Ошибка в поле ${field}: нарушение правила ${rule}, значение: ${value}`;

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

import { IsInt, Min } from "class-validator";

class Product {
  @IsInt({
    message: (args) =>
      buildMessage(args.property, "isInt", args.value)
  })
  price;
}

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

Обработка вложенных объектов

При работе с вложенными структурами ValidationError содержит children. Сообщения формируются рекурсивно.

class Address {
  @IsEmail()
  email;
}

class User {
  @ValidateNested()
  address;
}

Ошибка в address.email будет выглядеть как вложенная структура:

  • user

    • address

      • email: must be an email

Для формирования строки ошибки требуется обход дерева:

function flattenErrors(errors) {
  return errors.flatMap(error => {
    if (error.children && error.children.length) {
      return flattenErrors(error.children);
    }

    return Object.values(error.constraints || {});
  });
}

Кастомные валидаторы и сообщения

При создании собственного валидатора сообщение задаётся через ValidationArguments.

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

@ValidatorConstraint({ name: "isEven", async: false })
class IsEvenConstraint {
  validate(value) {
    return value % 2 === 0;
  }

  defaultMessage(args) {
    return `${args.property} должно быть чётным числом`;
  }
}

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

import { Validate } from "class-validator";

class NumberModel {
  @Validate(IsEvenConstraint)
  value;
}

Метод defaultMessage становится источником текста ошибки, если message не переопределён.

Приоритет сообщений

Система формирования сообщений имеет строгий порядок:

  1. message в декораторе
  2. defaultMessage кастомного валидатора
  3. стандартное сообщение библиотеки

Это позволяет комбинировать встроенные и пользовательские механизмы.

Использование функций-фабрик сообщений

Для сложных сценариев применяется функция, возвращающая функцию:

const minMessage = (min) => (args) =>
  `${args.property} не может быть меньше ${min}`;

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

import { Min } from "class-validator";

class Account {
  @Min(10, { message: minMessage(10) })
  balance;
}

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

Локализация сообщений

Один из частых сценариев — перевод ошибок.

const messages = {
  en: {
    required: "Field is required"
  },
  ru: {
    required: "Поле обязательно"
  }
};

const t = (lang, key) => messages[lang][key];

class Login {
  @IsNotEmpty({
    message: () => t("ru", "required")
  })
  username;
}

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

Контекст ошибки и расширенные данные

Объект ValidationArguments содержит дополнительные поля:

  • object — текущий экземпляр класса
  • value — проверяемое значение
  • constraints — параметры декоратора
  • targetName — имя класса

Это позволяет строить контекстные сообщения:

message: (args) =>
  `${args.targetName}.${args.property} содержит недопустимое значение`

Формирование агрегированных сообщений

При обработке API часто требуется преобразование массива ошибок в единый формат:

function formatErrors(errors) {
  return errors.map(err => ({
    field: err.property,
    errors: Object.values(err.constraints || {})
  }));
}

Результат:

[
  {
    "field": "email",
    "errors": ["email must be an email"]
  }
]

Использование message с асинхронной логикой

Хотя сам валидатор может быть асинхронным, сообщение остаётся синхронным. Однако допускается использование заранее подготовленных данных:

const dictionary = await loadDictionary();

const message = (args) =>
  dictionary[args.property] || "Ошибка валидации";

Преобразование сообщений на уровне сервиса

В архитектурах с сервисным слоем часто выполняется постобработка:

function mapValidationErrors(errors) {
  return errors.reduce((acc, err) => {
    if (err.constraints) {
      acc.push({
        field: err.property,
        message: Object.values(err.constraints)[0]
      });
    }
    return acc;
  }, []);
}

Особенности работы с массивами

При валидации массивов ошибки могут дублироваться на уровне индексов:

class User {
  @IsEmail({}, { each: true })
  emails;
}

Ошибки будут иметь вложенные children, где каждое сообщение относится к конкретному элементу массива.

Управление читаемостью сообщений

При проектировании сообщений учитываются следующие принципы:

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

Пример улучшенного сообщения:

message: (args) =>
  `Поле ${args.property} должно содержать корректный email-адрес`

Интеграция с трансформацией данных

При использовании transform (например, в связке с DTO) сообщения могут зависеть от приведённого типа данных. Это влияет на значение value, доступное в ValidationArguments, что позволяет формировать более точные ошибки:

message: (args) =>
  `Значение "${args.value}" не соответствует требованиям поля ${args.property}`