Библиотека формирует единый тип результата при нарушении правил
валидации — массив объектов ValidationError. Именно этот
формат становится основой всей дальнейшей обработки.
Каждый элемент содержит ключевые поля:
Типичная структура:
export interface ValidationError {
target?: object;
property: string;
value?: any;
constraints?: {
[type: string]: string;
};
children?: ValidationError[];
}
Именно поле constraints становится точкой входа для
формирования пользовательских сообщений.
После вызова validate() или
validateOrReject() результатом становится массив
ошибок.
import { validate } from "class-validator";
const errors = await validate(userDto);
Пустой массив означает успешную проверку. Непустой — наличие нарушений.
Простейший вариант обработки — извлечение всех текстов:
function extractMessages(errors) {
return errors.flatMap(error => {
if (!error.constraints) return [];
return Object.values(error.constraints);
});
}
Результат:
[
"email must be an email",
"password must be longer than 8 characters"
]
Вложенные структуры появляются при использовании:
@ValidateNested()Пример DTO:
class Address {
@IsString()
city: string;
}
class User {
@ValidateNested()
@Type(() => Address)
address: Address;
}
Ошибки в address попадут в children.
function flattenErrors(errors) {
const result = [];
const traverse = (errs) => {
for (const err of errs) {
if (err.constraints) {
result.push({
property: err.property,
messages: Object.values(err.constraints),
});
}
if (err.children && err.children.length) {
traverse(err.children);
}
}
};
traverse(errors);
return result;
}
В серверных приложениях часто требуется привести ошибки к единому формату ответа.
Пример стандартизации:
function formatValidationErrors(errors) {
const formatted = {};
for (const error of errors) {
if (error.constraints) {
formatted[error.property] = Object.values(error.constraints);
}
if (error.children?.length) {
formatted[error.property] = formatValidationErrors(error.children);
}
}
return formatted;
}
Результат:
{
"email": ["must be an email"],
"address": {
"city": ["should not be empty"]
}
}
Метод validateOrReject выбрасывает исключение при
наличии ошибок.
import { validateOrReject } from "class-validator";
await validateOrReject(dto);
При нарушении возвращается Promise.reject с массивом
ошибок.
try {
await validateOrReject(dto);
} catch (errors) {
console.log(errors);
}
Это удобно при построении сервисного слоя, где требуется прерывание выполнения.
app.post("/user", async (req, res) => {
const dto = plainToInstance(UserDto, req.body);
const errors = await validate(dto);
if (errors.length > 0) {
return res.status(400).json({
errors: formatValidationErrors(errors),
});
}
res.send("OK");
});
В рамках NestJS обработка централизуется через
ValidationPipe.
app.useGlobalPipes(new ValidationPipe());
При ошибке выбрасывается BadRequestException, содержащая
массив ValidationError.
NestJS позволяет изменить структуру ответа:
new ValidationPipe({
exceptionFactory: (errors) => {
const formatted = errors.map(err => ({
field: err.property,
messages: err.constraints
? Object.values(err.constraints)
: [],
}));
return new BadRequestException(formatted);
},
});
Это полностью переопределяет стандартный формат обработки.
Флаг:
new ValidationPipe({
stopAtFirstError: true,
});
Изменяет поведение валидации:
Это влияет на структуру массива: вместо полного дерева формируется минимальный результат.
Эти параметры влияют на генерацию ошибок на уровне полей DTO.
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
});
Поведение:
whitelist — удаляет лишние поляforbidNonWhitelisted — вызывает ошибку при наличии
лишних полейОшибки такого типа приходят как стандартные
ValidationError, но с системными сообщениями.
Декораторы позволяют задавать кастомные сообщения:
@IsEmail({}, { message: "Некорректный email-формат" })
email: string;
В constraints попадёт именно заданный текст.
Одно поле может иметь несколько правил:
@IsString()
@Length(5, 20)
password: string;
Результат:
constraints: {
isString: "password must be a string",
length: "password must be longer than 5 characters"
}
При обработке важно учитывать множественность значений, а не только первое сообщение.
При больших DTO полезна группировка:
Пример структуры:
{
user: {
email: ["invalid email"],
profile: {
age: ["must be a number"]
}
}
}
Такой формат формируется через рекурсивный обход
children.
При использовании @Validate() с async логикой ошибки
также возвращаются в стандартной форме:
@ValidatorConstraint({ async: true })
class UniqueEmailConstraint implements ValidatorConstraintInterface {
async validate(email: string) {
return false;
}
defaultMessage() {
return "Email already exists";
}
}
Ошибка попадёт в constraints как синхронная.
В продакшн-системах часто вводится слой трансформации:
ValidationError[] → DTO ошибки APIПример промежуточной модели:
type ApiValidationError = {
field: string;
messages: string[];
code?: string;
};
Поле target обычно исключается, поскольку содержит
исходный объект запроса.
Рекомендуемая фильтрация:
function sanitize(errors) {
return errors.map(({ property, constraints }) => ({
property,
messages: constraints ? Object.values(constraints) : [],
}));
}
Это предотвращает утечку внутреннего состояния объектов.
DTO с массивами:
class CreateOrder {
@ValidateNested({ each: true })
items: Item[];
}
Ошибки в этом случае содержат индексы:
items[0].price
items[1].name
При форматировании важно сохранять индексную структуру пути, иначе теряется контекст ошибки.