Библиотека class-validator возвращает ошибки валидации в
виде массива объектов типа ValidationError. Каждый такой
объект содержит полную информацию о том, какое поле не прошло проверку и
какие именно ограничения были нарушены.
Основные поля ValidationError:
Типичная структура ошибки:
{
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-формат, так как он легко индексируется и анализируется.
Пример интеграции с console:
function logValidationErrors(errors) {
const normalized = flattenValidationErrors(errors);
console.error(JSON.stringify({
type: "VALIDATION_ERROR",
timestamp: new Date().toISOString(),
errors: normalized
}));
}
Такой формат упрощает:
При использовании 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 ориентирован на высокую производительность и
минимальные накладные расходы.
import pino from "pino";
const logger = pino({ level: "error" });
function logValidationErrors(errors, meta) {
logger.error({
type: "VALIDATION_ERROR",
meta,
errors: flattenValidationErrors(errors)
});
}
Особенность подхода — отсутствие промежуточной сериализации, что важно при высоких нагрузках.
При использовании вложенных классов ошибки формируют дерево. Например:
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
}));
}
Это снижает риск утечки данных в логах.
При интеграции с сервером важно связывать ошибки с запросом.
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 или 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();
}
Такой подход централизует обработку ошибок и исключает дублирование логики.
При интенсивной нагрузке логирование может стать узким местом. Основные оптимизации:
Оптимизированный вариант:
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:
При накоплении данных возможна построение тепловых карт ошибок и выявление нестабильных контрактов между сервисами.
Для унификации логов применяется общий контракт:
{
type: "VALIDATION_ERROR",
timestamp,
context: {
route,
method,
service
},
errors: [
{
field,
messages,
value
}
]
}
Такой формат позволяет интегрировать class-validator в
распределённые системы без потери структуры данных и контекста.