Чтение и интерпретация ошибок валидации

Библиотека class-validator возвращает результаты проверки в виде массива объектов ValidationError. Каждый такой объект содержит информацию о нарушении правил валидации и может включать вложенные ошибки при работе с вложенными объектами.

Базовая структура ValidationError включает следующие ключевые поля:

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

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

ValidationError {
  property: "email",
  value: "not-an-email",
  constraints: {
    isEmail: "email must be an email"
  },
  children: [],
  target: UserDto { email: "not-an-email" }
}

Поле constraints и интерпретация нарушений

Ключевым элементом обработки ошибок является поле constraints. Оно представляет собой словарь, где:

  • ключ — имя валидатора (например, isEmail, minLength)
  • значение — сообщение об ошибке

Пример:

constraints: {
  isEmail: "email must be an email",
  minLength: "password is too short"
}

При множественных нарушениях одно свойство может содержать несколько записей в constraints. Порядок ключей не гарантируется, поэтому логика обработки не должна опираться на последовательность.


Вложенные ошибки и рекурсивная структура

Поле children используется при вложенной валидации объектов и массивов. В таких случаях структура ошибок становится деревом.

Пример DTO:

class Profile {
  @IsString()
  bio: string;
}

class User {
  @ValidateNested()
  profile: Profile;
}

Результирующая ошибка:

[
  {
    property: "profile",
    children: [
      {
        property: "bio",
        constraints: {
          isString: "bio must be a string"
        },
        children: []
      }
    ]
  }
]

Для корректной интерпретации таких структур требуется рекурсивный обход дерева ошибок.


Разворачивание дерева ошибок

Для приведения вложенной структуры к плоскому виду применяется рекурсивная обработка children.

Базовый алгоритм:

function flattenErrors(errors, parentPath = "") {
  return errors.flatMap(error => {
    const path = parentPath
      ? `${parentPath}.${error.property}`
      : error.property;

    const current = error.constraints
      ? [{
          property: path,
          messages: Object.values(error.constraints)
        }]
      : [];

    const nested = error.children?.length
      ? flattenErrors(error.children, path)
      : [];

    return [...current, ...nested];
  });
}

Результатом становится список ошибок с полными путями свойств:

profile.bio → bio must be a string
user.address.city → city should not be empty

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

Поле constraints часто используется как источник сообщений для API-ответов или логирования. Типичная обработка включает преобразование словаря в массив строк:

function extractMessages(error) {
  if (!error.constraints) return [];

  return Object.values(error.constraints);
}

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


Работа с contexts в кастомных валидаторах

Поле contexts появляется при использовании кастомных декораторов через ValidatorConstraint.

Пример:

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

  defaultMessage() {
    return "value must be even";
  }
}

При расширенной настройке можно передавать контекст:

contexts: {
  isEven: {
    expected: "even number",
    received: 3
  }
}

Интерпретация contexts позволяет:

  • добавлять метаданные в ошибки
  • формировать структурированные ответы API
  • реализовывать локализацию сообщений

Различие между target и value

  • target — исходный объект класса, проходящий валидацию
  • value — конкретное значение свойства, вызвавшего ошибку

Разделение важно при отладке сложных DTO, особенно при глубокой вложенности.

target: UserDto { email: "...", profile: {...} }
value: "..."

Типичные стратегии обработки ValidationError

Формирование API-ответа

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

function buildErrorResponse(errors) {
  return errors.map(err => ({
    field: err.property,
    errors: err.constraints
      ? Object.values(err.constraints)
      : []
  }));
}

Логирование структуры ошибок

Для серверного анализа сохраняется полная структура:

console.log(JSON.stringify(errors, null, 2));

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


Нормализация для фронтенда

Часто применяется формат:

{
  "email": ["must be an email"],
  "profile.bio": ["must be a string"]
}

Для этого используется объединение property и рекурсивный обход children.


Особенности поведения при нескольких декораторах

При применении нескольких валидаторов к одному полю:

@IsEmail()
@Length(10, 50)
email: string;

constraints содержит несколько записей одновременно:

constraints: {
  isEmail: "must be an email",
  length: "length must be between 10 and 50"
}

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


Пустые constraints и специфические случаи

Иногда объект ValidationError содержит пустые constraints при наличии вложенных ошибок в children. Это характерно для случаев:

  • @ValidateNested() без собственных правил
  • ошибки в глубоко вложенных DTO
  • кастомные валидаторы на уровне структуры

В таких случаях анализ выполняется только через children.


Рекурсивная природа ValidationError как ключевая особенность

Структура ошибок представляет собой дерево произвольной глубины. Это определяет необходимость:

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

Игнорирование вложенности приводит к потере части информации при сериализации и формировании ответов.