Одной из ключевых особенностей Zod является возможность строить сложные схемы из более простых за счёт композиции. Схемы в Zod не являются статичными структурами — они поддерживают цепочки методов, возвращающих новые экземпляры схем без изменения исходных.
import { z } from "zod";
const baseUser = z.object({
id: z.string(),
email: z.string().email(),
});
На основе базовой схемы можно создавать расширения без дублирования описаний.
const adminUser = baseUser.extend({
role: z.literal("admin"),
});
Такой подход формирует устойчивую архитектуру валидации, где базовые контракты данных переиспользуются на всех уровнях приложения.
Метод extend используется для добавления новых полей к
объектной схеме:
const user = z.object({
id: z.string(),
});
const userWithName = user.extend({
name: z.string(),
});
Исходная схема не изменяется, создаётся новая.
pick извлекает часть полей из схемы:
const userPublic = userWithName.pick({
id: true,
name: true,
});
omit исключает поля:
const userPrivate = userWithName.omit({
id: true,
});
Эти утилиты часто применяются для разделения публичных и внутренних DTO.
partial делает все поля необязательными:
const partialUser = userWithName.partial();
required восстанавливает обязательность:
const requiredUser = partialUser.required();
Также возможно частичное применение:
const onlyNameOptio nal = userWithName.partial({
name: true,
});
Контроль поведения лишних полей:
const strictSchema = z.object({
id: z.string(),
}).strict();
strict — запрещает лишние ключиpassthrough — сохраняет лишние ключиstrip — удаляет лишние ключи (поведение по
умолчанию)const passthroughSchema = z.object({
id: z.string(),
}).passthrough();
Метод transform позволяет менять форму данных после
валидации:
const stringToNumber = z.string().transform((val) => Number(val));
Схема сначала валидирует строку, затем преобразует её в число.
const userId = z.string().uuid().transform((id) => ({
id,
createdAt: Date.now(),
}));
preprocess применяется до валидации:
const numberSchema = z.preprocess((val) => {
if (typeof val === "string") return Number(val);
return val;
}, z.number());
Это полезно для нормализации входных данных.
pipe соединяет две схемы в цепочку преобразований:
const schema = z.string().pipe(z.number()).pipe(z.boolean());
Каждый этап получает результат предыдущего.
refine добавляет пользовательскую проверку:
const password = z.string().refine((val) => val.length >= 8, {
message: "Слишком короткий пароль",
});
Используется для простых бизнес-правил.
superRefine предоставляет доступ к контексту ошибок:
const schema = z.object({
password: z.string(),
confirm: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirm) {
ctx.addIssue({
path: ["confirm"],
message: "Пароли не совпадают",
code: z.ZodIssueCode.custom,
});
}
});
Позволяет добавлять множественные ошибки и указывать путь.
default задаёт значение при отсутствии поля:
const schema = z.object({
role: z.string().default("user"),
});
Если role не передан, он будет установлен
автоматически.
catch используется для fallback-значений при ошибке
парсинга:
const schema = z.number().catch(0);
Если значение невалидно, возвращается 0.
Также поддерживается функция:
z.number().catch(() => -1);
const schema = z.string().optional();
Поле может отсутствовать.
const schema = z.string().nullable();
Поле может быть null, но не undefined.
const schema = z.string().optional().nullable();
Допускает string | undefined | null.
Используется для рекурсивных структур:
const Category = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(Category).optional(),
})
);
Без lazy невозможно описать самоссылочные структуры.
Позволяет создавать номинальные типы:
const UserId = z.string().brand<"UserId">();
Хотя на уровне runtime это строка, TypeScript различает тип.
const schema = z.array(z.string()).readonly();
Используется для запрета мутаций на уровне типов.
Добавляет мета-информацию:
const schema = z.string().describe("Имя пользователя");
Описание может использоваться для генерации документации или форм.
Объединяет объектные схемы:
const a = z.object({ id: z.string() });
const b = z.object({ name: z.string() });
const merged = a.merge(b);
Создаёт пересечение типов:
const schema = z.intersection(a, b);
Результат должен соответствовать обеим схемам одновременно.
const schema = z.union([
z.string(),
z.number(),
]);
Поддерживает одно из нескольких значений.
Оптимизированный вариант union с дискриминатором:
const schema = z.discriminatedUnion("type", [
z.object({ type: z.literal("a"), value: z.string() }),
z.object({ type: z.literal("b"), value: z.number() }),
]);
Позволяет эффективно различать ветви.
const schema = z.custom((val) => typeof val === "bigint");
Используется для произвольной логики проверки.
Комбинирование утилит формирует выразительный слой описания данных:
const base = z.object({
id: z.string(),
createdAt: z.string(),
});
const updateSchema = base
.partial()
.extend({
updatedAt: z.string().optional(),
})
.refine((data) => Object.keys(data).length > 0);
Такие конструкции позволяют описывать изменения состояния данных без дублирования структур.
Схемы могут быть связаны в цепочки трансформаций и проверок:
const schema = z.string()
.transform((val) => val.trim())
.refine((val) => val.length > 0)
.pipe(z.string().min(3));
Каждый этап отвечает за отдельный аспект обработки данных: нормализацию, валидацию и финальную форму.