Одной из наиболее распространённых ошибок при работе с Zod является
использование parse там, где требуется безопасная обработка
ошибок. Метод parse выбрасывает исключение при невалидных
данных, что часто приводит к неожиданным падениям приложения.
import { z } from "zod";
const schema = z.object({
id: z.number(),
});
const data = JSON.parse('{"id":"not-a-number"}');
// Ошибка выбросится и прервёт выполнение
schema.parse(data);
Проблема усугубляется в серверных приложениях, где отсутствие обработки исключений может приводить к падению всего запроса.
Альтернативный подход:
const result = schema.safeParse(data);
if (!result.success) {
console.log(result.error.format());
} else {
console.log(result.data);
}
Использование safeParse позволяет централизованно
контролировать поток ошибок без прерывания выполнения программы.
optional, nullable и
defaultЧастая ошибка заключается в предположении, что эти модификаторы ведут себя одинаково. На практике их семантика существенно различается.
const schema = z.object({
a: z.string().optional(),
b: z.string().nullable(),
c: z.string().default("hello"),
});
Поведение:
optional() — поле может отсутствовать полностьюnullable() — поле может быть null, но не
отсутствоватьdefault() — подставляет значение только при
undefinedТипичная ошибка возникает при комбинации:
z.string().optional().default("x");
Фактически default не сработает, если значение
undefined уже интерпретировано как “отсутствует поле” в
определённых контекстах (например, при частичных объектах или
трансформациях).
transformtransform часто используется как способ “исправить”
данные, но приводит к скрытым ошибкам, когда типы перестают
соответствовать реальности.
const schema = z.string().transform((val) => val.length);
После трансформации тип становится number, но исходная
схема остаётся строковой.
Ошибка возникает при повторном использовании схемы:
type A = z.infer<typeof schema>; // string, но фактически number после transform
Решение заключается в разделении валидации и преобразования через явные промежуточные схемы или строгий контроль типов:
const base = z.string();
const transformed = base.transform((val) => val.length);
z.infer
и TypeScriptОдно из частых заблуждений — ожидание полной идентичности runtime и compile-time типов.
const schema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer<typeof schema>;
Проблема возникает при использовании refine,
transform, superRefine, где TypeScript не
всегда корректно отражает изменённую структуру.
const schema = z.string().transform((v) => Number(v));
type T = z.infer<typeof schema>; // number
Но при сложных композициях тип может становиться неточным или слишком
широким (unknown).
refine и superRefinerefine часто используется для бизнес-логики, но его
неправильное применение приводит к труднодиагностируемым ошибкам.
z.number().refine((val) => val > 10, {
message: "Too small",
});
Проблема возникает, когда требуется доступ к нескольким полям —
разработчики продолжают использовать refine вместо
superRefine.
z.object({
password: z.string(),
confirm: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirm) {
ctx.addIssue({
path: ["confirm"],
message: "Passwords do not match",
code: z.ZodIssueCode.custom,
});
}
});
Ошибка заключается в попытке реализовать подобную логику через
refine, что ограничивает доступ к контексту.
union и discriminatedUnionОбычные union часто вызывают проблемы при парсинге
неоднозначных структур.
const schema = z.union([
z.object({ type: z.literal("a"), a: z.string() }),
z.object({ type: z.literal("b"), b: z.number() }),
]);
Типичная ошибка — отсутствие дискриминатора или его неверное использование, что приводит к медленной проверке и неоднозначным ошибкам.
Рекомендуемая практика — использование
discriminatedUnion:
const schema = z.discriminatedUnion("type", [
z.object({ type: z.literal("a"), a: z.string() }),
z.object({ type: z.literal("b"), b: z.number() }),
]);
Ошибка часто возникает при попытке добавить дополнительные варианты без обновления дискриминатора, что ломает проверку.
strict, strip и
passthroughМногие разработчики ожидают, что Zod всегда удаляет лишние поля автоматически.
const schema = z.object({
id: z.number(),
}).strict();
Ошибка возникает при попытке передать дополнительные поля:
schema.parse({ id: 1, extra: true }); // ошибка
При этом:
strip (по умолчанию) удаляет лишние поляstrict выбрасывает ошибкуpassthrough сохраняет лишние поляЧастая ошибка — использование strict в API-слоях без
осознания последствий, что приводит к неожиданным падениям запросов.
deepPartialpartial() работает только на первом уровне, что часто
вызывает неправильные ожидания.
const schema = z.object({
user: z.object({
name: z.string(),
}),
}).partial();
Ошибка: name внутри user остаётся
обязательным.
Для корректного поведения требуется:
schema.deepPartial();
Игнорирование этого приводит к ошибкам при обработке частичных обновлений (PATCH-запросов).
z.lazy и рекурсивных схемРекурсивные структуры часто реализуются неправильно из-за отсутствия
lazy.
const schema = z.object({
name: z.string(),
child: schema, // ошибка
});
Корректный вариант:
const schema = z.object({
name: z.string(),
child: z.lazy(() => schema),
});
Типичная проблема — забывание оборачивания, что приводит к бесконечной рекурсии при инициализации схемы.
Частая ошибка — ожидание, что .array() автоматически
валидирует содержимое глубоко и строго.
z.array(z.number()).parse([1, "2", 3]);
Ошибка возникает в интерпретации: "2" не приводится
автоматически к числу.
Для приведения типов требуется coerce:
z.array(z.coerce.number());
coercecoerce часто применяется без понимания побочных
эффектов.
z.coerce.number().parse("abc");
Результат: NaN, который проходит валидацию как число в
некоторых сценариях.
Это приводит к скрытым багам, особенно при работе с API и формами.
environment variables схемамиТипичная ошибка при валидации переменных окружения:
const schema = z.object({
PORT: z.number(),
});
process.env.PORT всегда строка, что приводит к
постоянным ошибкам.
Корректный подход:
const schema = z.object({
PORT: z.coerce.number(),
});
Игнорирование этого приводит к нестабильной конфигурации приложения.
Схемы Zod являются иммутабельными, но их композиция может приводить к неожиданным эффектам при реиспользовании.
const base = z.object({ id: z.number() });
const extended = base.extend({
name: z.string(),
});
Ошибка возникает, когда предполагается, что base
изменяется — на практике создаётся новая схема.
Стандартный error.message часто используется
неправильно:
const result = schema.safeParse(data);
console.log(result.error.message);
Проблема: сообщение агрегированное и теряет структуру.
Правильный доступ:
console.log(result.error.format());
или
console.log(result.error.issues);
Игнорирование структуры ошибок приводит к невозможности корректного отображения проблем в UI.
async схемZod поддерживает асинхронную валидацию через refine, но
частая ошибка — смешивание sync и async API.
await schema.parseAsync(data);
Ошибка возникает при использовании parse вместо
parseAsync, что приводит к игнорированию
async-валидаций.
Глубокая композиция схем приводит к ухудшению читаемости и деградации производительности.
z.object({
a: z.object({
b: z.object({
c: z.object({
d: z.string(),
}),
}),
}),
});
Типичная ошибка — отсутствие декомпозиции и повторное использование вложенных структур, что усложняет сопровождение и тестирование.
brandbrand часто применяется для создания номинальных типов,
но забывается при последующих трансформациях.
const UserId = z.string().brand<"UserId">();
После transform бренд может теряться, что ломает типовую
безопасность на уровне компиляции.
preprocess и схемpreprocess часто используется как универсальный
инструмент нормализации данных, но приводит к скрытым багам.
const schema = z.preprocess((val) => Number(val), z.number());
Ошибка возникает, когда val уже является числом или
null, что приводит к неожиданным преобразованиям.