Логирование ошибок валидации

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

Основные поля ValidationError:

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

Типичная структура ошибки:

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

При работе с вложенными DTO структура усложняется, поскольку children формирует дерево ошибок. Это требует рекурсивной обработки при логировании.


Базовое логирование ошибок валидации

Простейший вариант логирования заключается в прямом выводе массива ошибок:

import { validate } from "class-validator";

async function validateUser(dto) {
  const errors = await validate(dto);

  if (errors.length > 0) {
    console.error("Validation errors:", errors);
  }

  return errors;
}

Такой подход даёт минимальную информацию и плохо подходит для продакшн-логирования, поскольку:

  • структура объектов сложна для чтения;
  • вложенные ошибки не раскрываются;
  • отсутствует контекст запроса.

Преобразование ошибок в плоский формат

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

function flattenValidationErrors(errors, parentPath = "") {
  const result = [];

  for (const error of errors) {
    const currentPath = parentPath
      ? `${parentPath}.${error.property}`
      : error.property;

    if (error.constraints) {
      result.push({
        field: currentPath,
        messages: Object.values(error.constraints),
        value: error.value
      });
    }

    if (error.children && error.children.length > 0) {
      result.push(...flattenValidationErrors(error.children, currentPath));
    }
  }

  return result;
}

Результат преобразования становится пригодным для структурированных логов:

[
  {
    "field": "user.email",
    "messages": ["email must be an email"],
    "value": "invalid"
  }
]

Структурированное логирование через JSON

Современные лог-системы предпочитают JSON-формат, так как он легко индексируется и анализируется.

Пример интеграции с console:

function logValidationErrors(errors) {
  const normalized = flattenValidationErrors(errors);

  console.error(JSON.stringify({
    type: "VALIDATION_ERROR",
    timestamp: new Date().toISOString(),
    errors: normalized
  }));
}

Такой формат упрощает:

  • поиск ошибок в логах;
  • построение метрик;
  • интеграцию с ELK / Loki / Datadog.

Логирование через Winston

При использовании winston структура логов становится более управляемой.

import winston from "winston";

const logger = winston.createLogger({
  level: "error",
  format: winston.format.json(),
  transports: [
    new winston.transports.Console()
  ]
});

function logValidationErrors(errors, context = {}) {
  logger.error("Validation failed", {
    context,
    errors: flattenValidationErrors(errors)
  });
}

Контекст позволяет фиксировать:

  • маршрут запроса;
  • идентификатор пользователя;
  • входные данные (при необходимости частично).

Логирование через Pino

pino ориентирован на высокую производительность и минимальные накладные расходы.

import pino from "pino";

const logger = pino({ level: "error" });

function logValidationErrors(errors, meta) {
  logger.error({
    type: "VALIDATION_ERROR",
    meta,
    errors: flattenValidationErrors(errors)
  });
}

Особенность подхода — отсутствие промежуточной сериализации, что важно при высоких нагрузках.


Рекурсивная обработка вложенных DTO

При использовании вложенных классов ошибки формируют дерево. Например:

class Address {
  @IsString()
  city;
}

class User {
  @ValidateNested()
  address;
}

Ошибки могут выглядеть так:

{
  property: "address",
  children: [
    {
      property: "city",
      constraints: {
        isString: "city must be a string"
      }
    }
  ]
}

Для логирования важно сохранять иерархию или корректно её разворачивать:

function formatTree(errors) {
  return errors.map(err => ({
    property: err.property,
    constraints: err.constraints,
    children: err.children ? formatTree(err.children) : []
  }));
}

Такой формат удобен для UI систем отображения ошибок.


Фильтрация чувствительных данных

Логирование значений value требует осторожности. Часто DTO содержит:

  • пароли
  • токены
  • персональные данные

Фильтрация:

const SENSITIVE_FIELDS = new Set(["password", "token", "secret"]);

function sanitizeErrors(errors) {
  return flattenValidationErrors(errors).map(err => ({
    ...err,
    value: SENSITIVE_FIELDS.has(err.field) ? "[REDACTED]" : err.value
  }));
}

Это снижает риск утечки данных в логах.


Контекстное логирование в HTTP-запросах

При интеграции с сервером важно связывать ошибки с запросом.

function logRequestValidation(req, errors) {
  logger.error("Request validation failed", {
    path: req.url,
    method: req.method,
    ip: req.ip,
    errors: flattenValidationErrors(errors)
  });
}

Добавление контекста позволяет:

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

Группировка ошибок по полям

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

function groupErrors(errors) {
  const flat = flattenValidationErrors(errors);

  return flat.reduce((acc, err) => {
    if (!acc[err.field]) {
      acc[err.field] = [];
    }
    acc[err.field].push(...err.messages);
    return acc;
  }, {});
}

Результат:

{
  "email": ["email must be an email"],
  "password": ["password is too short"]
}

Интеграция с middleware валидации

В типичных архитектурах валидация выполняется на уровне middleware или pipe.

async function validationMiddleware(dto, req, next) {
  const errors = await validate(dto);

  if (errors.length > 0) {
    logValidationErrors(errors, {
      url: req.url,
      method: req.method
    });

    throw new Error("Validation failed");
  }

  return next();
}

Такой подход централизует обработку ошибок и исключает дублирование логики.


Производительность логирования

При интенсивной нагрузке логирование может стать узким местом. Основные оптимизации:

  • предварительная фильтрация ошибок
  • использование JSON-логгеров вместо строковой конкатенации
  • минимизация рекурсивных проходов
  • отказ от глубокого клонирования объектов

Оптимизированный вариант:

function fastFlatten(errors, path = "") {
  let result = [];

  for (let i = 0; i < errors.length; i++) {
    const err = errors[i];
    const current = path ? `${path}.${err.property}` : err.property;

    if (err.constraints) {
      result.push(current);
    }

    if (err.children?.length) {
      result = result.concat(fastFlatten(err.children, current));
    }
  }

  return result;
}

Логирование как источник аналитики

Структурированные ошибки валидации используются не только для отладки, но и для анализа качества API:

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

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


Формирование единого формата ошибок

Для унификации логов применяется общий контракт:

{
  type: "VALIDATION_ERROR",
  timestamp,
  context: {
    route,
    method,
    service
  },
  errors: [
    {
      field,
      messages,
      value
    }
  ]
}

Такой формат позволяет интегрировать class-validator в распределённые системы без потери структуры данных и контекста.