Альтернатива Zod размером 1 КБ
19 марта 2025 года вышла стабильная версия Valibot — библиотеки для валидации данных в JavaScript/TypeScript. Разработанная как альтернатива популярному Zod, она сочетает минималистичный дизайн с мощными возможностями.
В этой статье мы сравним Valibot и Zod по трём ключевым параметрам: синтаксису API, размеру библиотеки и скорости работы. Вы узнаете, чем эти решения отличаются друг от друга и почему стоит использовать специализированные инструменты валидации входящих данных.
Зачем нужны библиотеки валидации вроде Valibot и Zod?
Когда ваше приложение взаимодействует с API, вы ожидаете данные в строго определённом формате. Какое-то время всё может работать идеально. Однако практика показывает, что рано или поздно происходят изменения: поля могут исчезнуть, тип значения может измениться, а в названиях полей появляются незаметные опечатки.
Последствия таких изменений варьируются от незначительных визуальных багов до полной неработоспособности приложения. Главный вопрос заключается не в том, произойдёт ли сбой, а в том, когда он случится и какие потери повлечёт за собой.
Перед разработчиком встаёт выбор: либо самостоятельно реализовывать сложные проверки для каждого типа входящих данных, что требует значительных временных затрат и не гарантирует полного охвата всех возможных сценариев, либо доверить эту задачу специализированным библиотекам. Такие решения, предлагают готовый механизм для описания ожидаемой структуры данных и автоматической проверки на соответствие этим требованиям.
Например, вместо написания громоздких условных конструкций для проверки каждого поля вручную, вы можете описать схему данных всего один раз. Этот подход не только сокращает объём кода, но и обеспечивает централизованное управление правилами валидации, что особенно ценно в крупных проектах со множеством интеграций.
Давайте разберём пример валидации ответа от API с использованием Valibot:
import * as v from 'valibot';
const ProductScheme = v.object({
title: v.string(),
price: v.number(),
rating: v.number(),
description: v.string(),
images: v.array(v.string())
});
type Product = v.InferOutput<typeof ProductScheme>;
const getProduct = async (id: string): Promise<Product> => {
const res: Product = await fetch(`https://dummyjson.com/products/${id}`)
.then((res) => res.json());
try {
return v.parse(ProductScheme, res);
} catch (error) {
console.error(error);
return res;
}
};
На первом этапе мы явно описываем ожидаемую структуру данных от API. На основе этого описания автоматически генерируется готовый к использованию TS-тип. Несмотря на то, что мы явно указываем тип переменной res
, без валидации мы не можем быть уверены в содержимом.
Далее мы пытаемся распарсить данные по нашей схеме. Valibot при обнаружении несоответствий выбрасывает ValieError
, объект ошибки содержит все найденные проблемы. После получения ошибки мы можем выполнить следующий набор действий:
Журналировать инцидент в системе мониторинга, например, в Sentry.
Попытаться обработать ошибку и восстановить работоспособность.
Использовать некорректные данные «как есть». Рискованный подход, но лучше, чем ничего. Возможно, просто не отобразится маленький кусочек интерфейса вместо недоступности всей страницы.
Valibot также существенно упрощает проверку пользовательского ввода — от простых форм до сложных структур данных. Рассмотрим пример реализации проверки данных для создания нового аккаунта перед отправкой их в API:
import * as v from 'valibot';
const NewAccountSchema = v.object({
email: v.pipe(v.string(), v.email()),
age: v.pipe(v.string(), v.digits(), v.minValue('18')),
password: v.pipe(v.string(), v.minLength(4), v.maxLength(12)),
repeatPassword: v.pipe(v.string(), v.minLength(4), v.maxLength(12))
});
type NewAccount = v.InferOutput<typeof NewAccountSchema>;
function isDataValid(accountData: NewAccount) {
const { issues, output, success } = v.safeParse(NewAccountSchema, accountData);
return { valid: success, output, issues: issues?.map((issue) => issue.message) };
}
isDataValid({
email: 'hello_world@whatever.com',
age: '40',
password: 'qwerty123',
repeatPassword: 'qwerty123'
});
Мы создали схему поверки с четырьмя полями: email должен быть строкой в корректном формате; возраст (передаваемый как строка) проверяется на то, что пользователю более 18 лет; а два пароля должны содержать строки длиной от 4 до 12 символов.
Чтобы обеспечить простую проверку данных из пользовательской формы без усложнения логики, первоначальную схему я намеренно упростил. В ней есть три существенных недостатка:
отсутствует проверка совпадения паролей;
поле возраста передаётся как строка, хотя вымышленный API ожидает числовой формат;
текущая система валидации возвращает ошибки в техническом формате, который сложен для понимания пользователям.
Давайте доработаем схему, чтобы устранить эти недостатки и привести её в соответствие с требованиями вымышленного API.
const NewAccountSchema = v.pipe(
v.object({
email: v.pipe(
v.string('Email-адрес должен в формате строки'),
v.email('Введите корректный email-адрес в формате: example@domain.com')
),
age: v.pipe(
v.string('Возраст должен быть в формате строки'),
v.digits('Возраст должен быть числом'),
v.transform(Number),
v.minValue(18, 'Доступ на сайт разрешен с 18 и более лет')
),
password: v.pipe(
v.string('Пароль должен быть в формате строки'),
v.minLength(4, 'Минимальная длина пароля 4 символа'),
v.maxLength(12, 'Максимальная длинна пароля 12 символов')
),
repeatPassword: v.string()
}),
v.forward(
v.partialCheck(
[['password'], ['repeatPassword']],
({ password, repeatPassword }) => password === repeatPassword,
'Пароли не совпадают, исправьте значения таким образом что бы они стали равны друг другу'
),
['repeatPassword']
)
);
Наша схема теперь полностью соответствует всем требованиям:
ошибки при проверке выводятся в удобном для пользователя формате;
текстовое числовое значение поля
age
автоматически преобразуется в числовой тип;добавлена строгая проверка равенства полей
password
иrepeatPassword
: теперь мы гарантируем их полное совпадение без необходимости дублирования правил проверки.
Рассмотрим разные результаты проверки:
// Позитивный сценарий
isDataValid({
email: 'hello_world@whatever.com',
age: '23',
password: 'qwerty123',
repeatPassword: 'qwerty123'
});
/*
{
valid: true,
output: {
email: 'hello_world@whatever.com',
age: 23,
password: 'qwerty123',
repeatPassword: 'qwerty123'
},
issues: undefined
}
*/
// Негативный сценарий
isDataValid({
email: 'hello_world@whatever',
age: 'test',
password: '23',
repeatPassword: 'qwerty123'
});
/*
{
valid: false,
output: {
email: 'hello_world@whatever',
age: 'test',
password: '23',
repeatPassword: 'qwerty123'
},
issues: [
'Введите корректный email-адрес в формате: example@domain.com',
'Возраст должен быть в формате числа',
'Минимальная длина пароля 4 символа'
]
}
*/
Теперь данные проверяются на клиенте перед отправкой, мы можем показать ошибки пользователю и предотвратить вызов API с некорректными данными, уменьшая паразитный трафик.
Реже, но всё же встречаются сценарии корректных данных из
cookies
иlocalStorage
. Кроме того, это полезно для проверки query‑параметров (таких как?id=123&mode=edit
), где важно контролировать типы данных и допустимые значения (например,mode
может быть толькоview
илиedit
).То же касается и обработки ошибок конфигурации, особенно при разработке npm‑пакетов, где валидация входных параметров помогает избежать неочевидных сбоев.
P.S. Мой коллега @frontendmma не так давно писал про пользу библиотек валидации при написании Backend-for-Frontend. Читать тут.
Надеюсь, это введение мотивирует разработчиков, которые пока не используют библиотеки для проверки данных, попробовать их в работе. А теперь давайте сравним Valibot и Zod.
Valibot и Zod в числах
Сравнение веса
Я протестировал Valibot на практике, взяв средний по размеру проект, изначально написанный на Zod. В проекте было около двадцати API-запросов с различными типами данных, и несколько форм, идеально подходящих для сравнения библиотек.
После перехода на Valibot вес бандла в gzip уменьшился на 12,8 КБ — ровно на разницу в весе импортируемых библиотек. При этом исходный код почти не изменился, поскольку API у Valibot и Zod очень похож.
1:0 в пользу Valibot.
Сравнение скорости работы
Roughly speaking, the library is about twice as fast as Zod.
Давайте проверим это утверждение на практике. Я написал идентичный скрипт проверки формы с использованием обеих библиотек. Исходный код приведён ниже.
Код
import * as v from 'valibot';
function runTest() {
const ProductScheme = v.object({
title: v.string(),
price: v.number(),
rating: v.number(),
description: v.string(),
images: v.array(v.string())
});
type Product = v.InferOutput<typeof ProductScheme>;
const testData: Product = {
title: 'bmw',
price: 10000000,
rating: 4.68,
description:
'Оригинальные аксессуары для вашей семьи и вашего автомобиля BMW. Перейти в магазин. Выбор и покупка. Модельный ряд · Тест-драйв · Автомобили с пробегом · BMW ...',
images: [
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/bmw-z4-m40i/2022/Navigation/bmw-z-series-z4-m40i-roadster-modelfinder.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1664317823574.png',
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/xm/2023/navigation/bmw_x-series_xm_modelcard_50.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1681276548689.png',
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/x7-m60i/2022/navigation/bmw-x-series-x7-m60i-modellfinder.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1649764340453.png'
]
};
v.parse(ProductScheme, testData);
}
let i = 0;
const t0 = performance.now();
while (i < 10000) {
runTest();
i++;
}
const t1 = performance.now();
console.info(t1 - t0, 'ms');
import { z } from 'zod';
function runTest() {
const ProductScheme = z.object({
title: z.string(),
price: z.number(),
rating: z.number(),
description: z.string(),
images: z.array(z.string())
});
type Product = z.infer<typeof ProductScheme>;
const testData: Product = {
title: 'bmw',
price: 10000000,
rating: 4.68,
description:
'Оригинальные аксессуары для вашей семьи и вашего автомобиля BMW. Перейти в магазин. Выбор и покупка. Модельный ряд · Тест-драйв · Автомобили с пробегом · BMW ...',
images: [
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/bmw-z4-m40i/2022/Navigation/bmw-z-series-z4-m40i-roadster-modelfinder.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1664317823574.png',
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/xm/2023/navigation/bmw_x-series_xm_modelcard_50.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1681276548689.png',
'https://www.bmw.kg/content/dam/bmw/common/all-models/m-series/x7-m60i/2022/navigation/bmw-x-series-x7-m60i-modellfinder.png/jcr:content/renditions/cq5dam.resized.img.585.low.time1649764340453.png'
]
};
ProductScheme.parse(testData);
}
let i = 0;
const t0 = performance.now();
while (i < 10000) {
runTest();
i++;
}
const t1 = performance.now();
console.info(t1 - t0, 'ms');
Посмотрим на минимальное и максимальное время работы по результатам запуска 10 итераций.
min (мс) | max (мс) | |
Valibot | 26,58 | 27,31 |
Zod | 67,02 | 70,25 |
Результаты оказались выше ожидаемых.
Теперь проведём аналогичную проверку, но с преднамеренно нарушенной структурой данных. В качестве значения для поля title
укажем число 1 вместо ожидаемой строки.
min (мс) | max (мс) | |
Valibot | 48,63 | 54,70 |
Zod | 143,63 | 147,79 |
Хотя в Valibot, в отличие от Zod, реализован механизм остановки проверки при первой ошибке, на результат это особо не повлияло. Наверное, схема должна быть просто огромной, чтобы разница стала заметной.
P. S. В Zod действительно есть похожая функциональность в методе
superRefine
, но это не то, что нам нужно.
2:0 в пользу Valibot.
Сравнение популярности
По данным npm, еженедельное количество загрузок библиотек сильно различается:
Valibot — 1,2 млн загрузок;
Zod — 24,5 млн загрузок.
О чём на самом деле говорит разница в популярности?
Да, Zod скачивают в 20 раз больше, но это ожидаемо, учитывая, что он имеет фору в несколько лет, а Valibot только набирает популярность. Но, думаю, вы согласитесь — так или иначе, количество установок всё же добавляет уверенности в инструменте.
Кроме того, популярность библиотеки способствует развитию экосистемы: появляются специализированные инструменты, упрощающие работу с ней. Особенно удобства добавляют такие инструменты как JSON-to-Zod.
2:1 не в пользу Zod.
Valibot vs Zod в коде
Рассмотрим простые примеры для наглядности.
import * as v from 'valibot';
const ProductScheme = v.object({
title: v.string(),
price: v.number()
});
type Product = v.InferOutput<typeof ProductScheme>;
const testData: Product = {
title: 'bmw',
price: 10000000
};
v.parse(ProductScheme, testData);
import { z } from 'zod';
const ProductScheme = z.object({
title: z.string(),
price: z.number()
});
type Product = z.infer<typeof ProductScheme>;
const testData: Product = {
title: 'bmw',
price: 10000000
};
ProductScheme.parse(testData);
Похожи, не правда ли? Но ключевые отличия кроются в деталях. Немного усложним примеры.
const ProductScheme = v.object({
title: v.pipe(
v.string('title должен быть строкой'),
v.minLength(3, 'title должен содержать 3 и более символов')
),
price: v.pipe(
v.number('price должен быть числом'),
v.minValue(3000000, 'price начинается от 3 🍋')
),
});
const ProductScheme = z.object({
title: z
.string({ message: 'title должен быть строкой' })
.min(3, 'title должен содержать 3 и более символов'),
price: z
.number({ message: 'price должен быть числом' })
.min(3000000, 'price начинается от 3 🍋')
});
Valibot использует модульный подход: каждая функция валидации (например, string()
, minLength()
) является независимым модулем. Благодаря этому tree‑shaking может исключать неиспользуемый код из production‑сборки. В отличие от этого, архитектура Zod построена на цепочных методах (вроде z.string().min()
), что делает подобную оптимизацию практически невозможной.
Когда речь заходит о проверках, требующих асинхронных операций (например, проверка доступности username), важно учитывать различия библиотек:
Valibot реализует эту функциональность через асинхронные валидаторы. Это интуитивно понятно: ведь всё, что вам нужно сделать — использовать постфикс
Async
.Zod поддерживает асинхронные проверки через метод
refine
, что тоже выглядит вполне изящно.
import * as v from 'valibot';
function isUsernameAvailable(username: string): Promise<boolean> {
return new Promise((resolve) => setTimeout(() => resolve(username.length > 5), 1000));
}
const NameSchema = v.pipeAsync(
v.string(),
v.checkAsync(isUsernameAvailable, 'Пользователь с таким именем уже существует')
);
v.parseAsync(NameSchema, 'jquery_dlya_slabih'); // успех
v.parseAsync(NameSchema, 'jq'); // ошибка
import { z } from 'zod';
function isUsernameAvailable(username: string): Promise<boolean> {
return new Promise((resolve) => setTimeout(() => resolve(username.length > 5), 1000));
}
const NameSchema = z
.string()
.refine(isUsernameAvailable, 'Пользователь с таким именем уже существует');
NameSchema.parseAsync('jquery_dlya_slabih'); // успех
NameSchema.parseAsync('jqu'); // ошибка
Zod@4?
В дополнение я протестировал бета-версию Zod 4. Несмотря на заявленные улучшения в производительности и уменьшение размера, мои тесты не подтвердили метрики, указанные на официальном сайте.
Например, zod@next
в production-сборке показал изменение веса всего на 1 КБ, при этом производительность ухудшилась в двадцать раз.
@zod/mini@next
продемонстрировал более заметное уменьшение размера, но всё равно остаётся на 5 КБ больше, чем Valibot. Также наблюдается двадцатикратное снижение производительности.
В целом, стоит дождаться стабильного релиза и только затем делать окончательные замеры. Вполне возможно, попался сломанный билд.
Подводим итоги
Использование специализированных библиотек проверки — это не просто вопрос удобства, а важная мера обеспечения надёжности вашего приложения. Они выступают в роли защитного слоя, который своевременно обнаруживает проблемы в данных, прежде чем те смогут повлиять на работу системы или пользовательский опыт.
Valibot — действительно достойная альтернатива Zod. Но насколько критична разница между ними? Честно говоря, я уверен: для большинства наших с вами веб-сайтов и приложений эти 12 КБ и доли миллисекунд на проверку данных — не самые главные проблемы.
Однако, если рассматривать написание нового или перенос небольшого проекта на Valibot:
Зачем платить больше?