Переход с Yup на Zod в проектах на JavaScript и TypeScript чаще всего связан с необходимостью усиления типизации, повышения предсказуемости схем валидации и более тесной интеграции с возможностями TypeScript. Архитектурно обе библиотеки решают одну задачу — описание и проверку структур данных, однако различаются философией, подходами к типам и способом построения схем.
В экосистеме валидации данных Yup долгое время использовался как стандарт де-факто для декларативного описания схем. Он ориентирован на удобство и цепочный API, однако в крупных TypeScript-проектах проявляются ограничения:
В результате при росте кодовой базы появляется расхождение между типами TypeScript и фактической схемой валидации, что увеличивает вероятность рассинхронизации логики.
Zod строится на иной концепции: схема является одновременно источником истины для типов и рантайм-валидации. Основные различия проявляются на уровне архитектуры:
InferType,
часто не совпадают с реальной схемой при сложных композицияхz.infer,
обеспечивая строгую синхронизацию.string().required()z.string().min(1)Перед заменой библиотеки важно зафиксировать существующие схемы и их поведение. Обычно выделяются следующие категории:
when)Дополнительно фиксируются типы, используемые в бизнес-логике, поскольку именно они будут сопоставляться с новой системой типов.
| Yup | Zod |
|---|---|
yup.string() |
z.string() |
yup.number() |
z.number() |
yup.boolean() |
z.boolean() |
yup.array() |
z.array() |
yup.object() |
z.object() |
yup.mixed() |
z.any() или z.unknown() |
Ключевое отличие проявляется в методах модификации:
.required(), .nullable().optional(), .nullable(),
.default()Базовая строковая схема:
Yup:
yup.string().required().min(3)
Zod:
z.string().min(3)
В Zod обязательность по умолчанию является частью контракта:
отсутствие .optional() означает обязательное поле.
Числовая схема:
Yup:
yup.number().positive().integer()
Zod:
z.number().positive().int()
Разница заключается в более строгом разделении методов и отсутствии неоднозначных комбинаций.
Сложные структуры в Yup часто строятся через
yup.object().shape({}).
Пример:
yup.object({
user: yup.object({
name: yup.string().required(),
age: yup.number().min(18)
})
})
Эквивалент в Zod:
z.object({
user: z.object({
name: z.string(),
age: z.number().min(18)
})
})
Ключевое отличие проявляется в типизации: Zod автоматически выводит точный тип вложенного объекта без дополнительных утилит.
Yup:
yup.array().of(yup.string().required())
Zod:
z.array(z.string())
В Zod отсутствие необходимости явно указывать required
упрощает композицию схем.
В Yup часто применяется when:
yup.string().when("role", {
is: "admin",
then: schema => schema.required()
})
В Zod условная логика реализуется через
z.discriminatedUnion или refine:
z.object({
role: z.string(),
value: z.string().optional()
}).refine(data => data.role !== "admin" || !!data.value)
Более строгий вариант:
z.discriminatedUnion("role", [
z.object({ role: z.literal("admin"), value: z.string() }),
z.object({ role: z.literal("user") })
])
В Yup:
yup.string().test("unique", async value => {
return await checkUnique(value)
})
В Zod:
z.string().refine(async value => {
return await checkUnique(value)
})
Zod требует явного использования parseAsync, что делает
асинхронное поведение предсказуемым и отделённым от синхронной
валидации.
Одним из ключевых факторов перехода является строгая интеграция с TypeScript.
В Zod тип извлекается напрямую:
const schema = z.object({
name: z.string(),
age: z.number()
})
type User = z.infer<typeof schema>
В Yup требуется дополнительный шаг:
type User = yup.InferType<typeof schema>
Однако различие не только синтаксическое. В Zod тип всегда полностью соответствует схеме без побочных расхождений.
Yup возвращает ошибки в виде массива строк или объектов с минимальной структурой.
Zod формирует детализированное дерево ошибок:
{
issues: [
{
path: ["user", "name"],
message: "Invalid input",
code: "invalid_type"
}
]
}
Это упрощает построение UI-валидации, особенно в формах с вложенной структурой.
Переход редко выполняется одномоментно. Обычно используется поэтапный подход:
Сначала Yup-схемы сохраняются, а новые модули пишутся на Zod.
Обе библиотеки применяются одновременно для критических участков системы.
Модули делятся на домены: формы, API, модели данных. Каждый домен мигрируется отдельно.
После полного покрытия тестами Yup исключается из зависимостей.
В связке с формами часто используется React Hook Form.
Yup:
resolver: yupResolver(schema)
Zod:
resolver: zodResolver(schema)
Zod обеспечивает более строгую типизацию формы, особенно при использовании generics.
Одним из ключевых преимуществ Zod является композиция:
const base = z.object({
id: z.string()
})
const extended = base.extend({
name: z.string()
})
В Yup аналог требует пересоздания объекта через .concat,
что менее прозрачно.
Zod предоставляет встроенные операции:
z.intersection(schemaA, schemaB)
z.union([schemaA, schemaB])
В Yup подобные операции реализуются менее явно и требуют кастомных решений.
Yup:
yup.string().default("guest")
Zod:
z.string().default("guest")
Однако Zod возвращает значение с учётом типизации результата, включая
корректное выведение string вместо
string | undefined.
При переходе часто возникают следующие сложности:
nullable и optionalparseAsynctransformstripUnknownZod по умолчанию более строг в отношении неизвестных полей, что требует явного использования:
z.object({...}).passthrough()
или
z.object({...}).strict()
Yup:
transform(value => value.trim())
Zod:
z.string().transform(value => value.trim())
Zod сохраняет цепочку типизации даже после трансформации, что снижает риск потери корректного типа.
После перехода на Zod структура валидации становится более предсказуемой:
Миграция приводит к выравниванию доменной модели данных и схем валидации, что уменьшает количество скрытых несоответствий между слоями приложения.