Валидация данных в Node-сервисе: Zod, Joi, Ajv и io-ts
Четыре библиотеки валидации для Node решают одну задачу по-разному: одни выводят типы автоматически, другие быстрее на больших объёмах. Разбираем цену каждого выбора.

В Node-сервисе на вход приходит JSON от клиента, из очереди или от внешнего API. Пока валидации нет, любой лишний ключ или строка вместо числа доходит до бизнес-логики и падает где-нибудь в глубине, с невнятным TypeError: cannot read properties of undefined. Первое очевидное решение — руками проверять if (typeof body.email !== 'string') на каждом поле. Оно перестаёт работать на третьем эндпоинте: проверок больше, чем логики, а типы для TypeScript всё равно приходится описывать отдельно и держать синхронными вручную.
Здесь мы сравниваем четыре популярных подхода к валидации в Node 20.x и рассматриваем только валидацию входных данных на границе сервиса. Генерация форм, валидация на клиенте и ORM-схемы — отдельные темы, их не трогаем.
Что вообще сравниваем
Все четыре библиотеки делают одно: описываешь ожидаемую форму данных, прогоняешь через неё вход, получаешь либо разобранный объект, либо ошибку. Разница в трёх вещах: как связаны схема и TypeScript-тип, насколько библиотека быстра и насколько богат язык описания.
- Zod — схема первична, тип выводится из неё через
z.infer. Ноль дублирования между рантаймом и компайл-таймом. - Joi — зрелая библиотека из экосистемы hapi, богатый DSL, но типы TypeScript выводятся слабо и часто описываются руками.
- Ajv — валидатор JSON Schema, компилирует схему в функцию, самый быстрый на потоке. Схема — это JSON, а не код.
- io-ts — из мира fp-ts, кодеки и функциональная композиция. Мощно для тех, кто живёт в Either и Task, тяжело для остальных.
Zod: тип и валидация из одного источника
Главное преимущество Zod в том, что вы пишете схему один раз, а тип получаете бесплатно. Меняете схему — тип меняется сам, рассинхрона быть не может.
// zod ^3.23
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
role: z.enum(['admin', 'user']).default('user'),
});
// Тип выводится из схемы, отдельно описывать не нужно
type CreateUser = z.infer<typeof createUserSchema>;
// parse бросает ZodError, safeParse возвращает объект с success
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
// result.error.issues — массив с путём и сообщением по каждой ошибке
return res.status(400).json({ errors: result.error.issues });
}
const user: CreateUser = result.data;Цена: Zod медленнее Ajv на больших объёмах, потому что интерпретирует схему при каждом вызове, а не компилирует её в функцию. На одиночных HTTP-запросах разница незаметна, на разборе миллиона строк из очереди — заметна.
Joi: богатый DSL, слабые типы
Joi существует дольше остальных и умеет описывать сложные зависимости между полями декларативно — например «поле B обязательно, если A равно определённому значению».
// joi ^17.13
import Joi from 'joi';
const schema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().integer().min(18).required(),
role: Joi.string().valid('admin', 'user').default('user'),
});
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
// error.details — массив с message и path по каждому нарушению
return res.status(400).json({ errors: error.details });
}Цена: тип value здесь будет any, если не описать его руками. То есть в TypeScript-проекте вы либо дублируете форму данных в интерфейсе, либо теряете типизацию. Для чистого JavaScript это не проблема, для строго типизированного кода — постоянный источник рассинхрона.
Ajv: скорость через компиляцию
Ajv компилирует JSON Schema в JavaScript-функцию один раз, а дальше вызывает её без интерпретации схемы. На большом потоке однотипных объектов это даёт заметный выигрыш.
// ajv ^8.17
import Ajv from 'ajv';
import addFormats from 'ajv-formats'; // отдельный пакет для email, date-time и т.п.
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 18 },
role: { type: 'string', enum: ['admin', 'user'] },
},
required: ['email', 'age'],
additionalProperties: false,
};
// Компилируем один раз при старте, не на каждый запрос
const validate = ajv.compile(schema);
if (!validate(req.body)) {
return res.status(400).json({ errors: validate.errors });
}Обратите внимание: форматы вроде email и date-time не входят в ядро Ajv, для них нужен пакет ajv-formats. Цена Ajv — схема на JSON Schema менее выразительна, чем код, а типы придётся выводить через отдельный пакет (json-schema-to-ts) или описывать руками. И компилировать схему нужно на старте, а не внутри обработчика — иначе весь выигрыш в скорости теряется.
io-ts: для функционального стека
io-ts возвращает результат как Either из fp-ts: слева ошибки, справа разобранное значение. Это естественно ложится на код, который уже построен вокруг функциональной композиции, и чужеродно во всём остальном.
Если в вашем проекте нет fp-ts и никто в команде не пишет на Either и pipe, io-ts добавит крутую кривую обучения ради валидации, которую Zod закрывает без единого нового понятия.
Таблица сравнения
| Критерий | Zod | Joi | Ajv | io-ts |
|---|---|---|---|---|
| Вывод TS-типов | Автоматически | Слабый, руками | Через доп. пакет | Автоматически |
| Скорость на потоке | Средняя | Средняя | Высокая | Средняя |
| Язык схемы | Код | Код (DSL) | JSON Schema | Код (кодеки) |
| Кривая обучения | Низкая | Низкая | Средняя | Высокая |
| Формат ошибок | issues[] | details[] | errors[] | Either |
Когда что выбирать
Критерий выбора не «какая лучше», а какое ограничение для вас критично.
- TypeScript-проект, обычная нагрузка HTTP-API — берите Zod. Один источник истины для типа и валидации перевешивает всё остальное, а разница в скорости на одиночных запросах не видна.
- Высокий поток однотипных объектов (парсинг больших файлов, обработка из Kafka/очереди, десятки тысяч объектов в секунду) — Ajv. Компиляция схемы окупается, замерьте на своих данных, прежде чем менять.
- Проект на чистом JavaScript без TypeScript — Joi или Zod, разница в типах вас не касается, выбирайте по вкусу к синтаксису.
- Кодовая база уже на fp-ts — io-ts впишется органично, в остальных случаях он избыточен.
Что ломается на практике
Типичные ошибки не в выборе библиотеки, а в том, как её встраивают.
- Компиляция схемы внутри обработчика. В Ajv
ajv.compile(schema)внутри роут-хендлера убивает всю производительность: схема пересобирается на каждый запрос. Компилируйте на старте модуля. - Лишние поля проходят молча. По умолчанию Zod и Joi не режут неописанные ключи, а Ajv без
additionalProperties: falseих пропускает. Если ждёте только известные поля — включайте строгий режим (.strict()в Zod,additionalProperties: falseв JSON Schema). - Первая ошибка вместо всех. Joi по умолчанию останавливается на первой (
abortEarly: true). Для формы, где хочется показать пользователю все проблемы сразу, ставьтеabortEarly: false, в Ajv —allErrors: true. - Числа как строки. Данные из query-параметров и form-urlencoded приходят строками.
z.number()отклонит"25". Нужен коэрсинг:z.coerce.number()в Zod,Joi.number()справляется с этим сам.
Что делать дальше
Вынесите валидацию в middleware, а не размазывайте по обработчикам: одна функция, которая принимает схему и валидирует req.body, req.query и req.params, и единый формат ошибки на выходе. Дальше — решите, где проводить границу между валидацией входа и бизнес-правилами: библиотека проверит, что age — целое число не меньше 18, но «этот email уже занят» относится к домену, а не к схеме. Если упрётесь в скорость — снимите замер на своих реальных данных через node --prof или простой цикл с performance.now(), прежде чем менять Zod на Ajv: часто узкое место оказывается не в валидации.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Zod — официальная документация, Ajv — JSON Schema validator
Частые вопросы
А если у меня старая версия — Zod 2 или Joi 16?
Валидация заметно замедлит мой API?
Можно ли использовать одну схему на клиенте и на сервере?
Что делать, если валидация проходит, а данные всё равно ломают логику?
Zod и Ajv можно совмещать в одном проекте?
Почему email в Ajv не работает из коробки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.