Динамические схемы в Zod строятся вокруг идеи того, что структура валидатора не обязана быть статичной на этапе компиляции. В реальных приложениях форма данных часто зависит от внешних факторов: конфигурации, версии API, пользовательских настроек, ответов сервера или комбинации нескольких источников.
Основной подход — генерация схемы через функции. Вместо объявления фиксированного объекта используется фабрика схем:
import { z } from "zod";
const createUserSchema = (minAge) =>
z.object({
name: z.string(),
age: z.number().min(minAge),
});
const schema = createUserSchema(18);
Такой подход позволяет параметризовать ограничения без дублирования кода. Валидация становится зависимой от входных условий, но сохраняет строгую типизацию.
Часто структура данных зависит от значений других полей или внешнего контекста. В Zod это реализуется через комбинацию фабрик схем и условной логики.
const createPaymentSchema = (method) => {
if (method === "card") {
return z.object({
method: z.literal("card"),
cardNumber: z.string(),
cvv: z.string(),
});
}
if (method === "paypal") {
return z.object({
method: z.literal("paypal"),
email: z.string().email(),
});
}
return z.object({
method: z.literal("cash"),
});
};
Такой подход используется при интеграции с API, где разные типы запросов требуют различных структур данных.
Схемы объектов можно собирать программно, добавляя поля на основе конфигурации.
const baseSchema = {
id: z.string(),
createdAt: z.date(),
};
const withOptionalFields = (flags) => {
const schemaShape = { ...baseSchema };
if (flags.includeEmail) {
schemaShape.email = z.string().email();
}
if (flags.includeRole) {
schemaShape.role = z.enum(["admin", "user"]);
}
return z.object(schemaShape);
};
Такой способ часто применяется в системах с модульными правами доступа или настраиваемыми профилями данных.
Динамические схемы часто пересекаются с рекурсивными структурами. Особенно это заметно в деревьях, комментариях, меню и графах.
const categorySchema = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(categorySchema).optional(),
})
);
z.lazy откладывает вычисление схемы до момента
использования, позволяя ссылаться на саму себя.
Это критично для структур, где глубина неизвестна заранее и формируется во время выполнения программы.
Массивы могут содержать элементы, тип которых определяется во время выполнения.
const createArraySchema = (type) => {
if (type === "string") return z.array(z.string());
if (type === "number") return z.array(z.number());
return z.array(z.any());
};
Более сложные сценарии используют комбинирование схем:
const createMixedArraySchema = (config) =>
z.array(
z.union([
config.allowStrings && z.string(),
config.allowNumbers && z.number(),
].filter(Boolean))
);
Такой подход позволяет строить гибкие структуры данных, адаптирующиеся под контекст.
Когда структура объекта неизвестна заранее, используется
z.record. Это позволяет описывать словари с динамическими
ключами.
const schema = z.record(z.string(), z.number());
В более сложных сценариях ключи могут зависеть от бизнес-логики:
const createSettingsSchema = (allowedKeys) =>
z.record(
z.enum(allowedKeys),
z.union([z.string(), z.number(), z.boolean()])
);
Такая конструкция применяется в конфигурационных системах, где набор параметров может изменяться между версиями приложения.
Объединения схем могут формироваться во время выполнения.
const createUnionSchema = (types) => {
const schemas = [];
if (types.includes("user")) {
schemas.push(z.object({ type: z.literal("user"), name: z.string() }));
}
if (types.includes("admin")) {
schemas.push(z.object({ type: z.literal("admin"), permissions: z.array(z.string()) }));
}
return z.union(schemas);
};
Это особенно полезно при обработке ответов API, где набор возможных вариантов расширяется со временем.
Когда у объектов есть дискриминатор (например, поле
type), Zod позволяет строить более строгие структуры:
const createEventSchema = (enabledEvents) => {
const schemas = [];
if (enabledEvents.includes("click")) {
schemas.push(
z.object({
type: z.literal("click"),
x: z.number(),
y: z.number(),
})
);
}
if (enabledEvents.includes("scroll")) {
schemas.push(
z.object({
type: z.literal("scroll"),
position: z.number(),
})
);
}
return z.discriminatedUnion("type", schemas);
};
Такая структура позволяет сохранять строгую типизацию даже при динамическом наборе вариантов.
Динамические схемы часто требуют предварительной нормализации входных данных.
const schema = z.preprocess((input) => {
if (typeof input === "string") {
return JSON.parse(input);
}
return input;
}, z.object({
id: z.string(),
}));
Этот механизм позволяет адаптировать данные из нестабильных источников: форм, API или внешних сервисов.
В реальных приложениях схемы часто строятся из конфигурационных файлов или описаний API.
const buildSchemaFromConfig = (config) => {
const shape = {};
for (const field of config.fields) {
switch (field.type) {
case "string":
shape[field.name] = z.string();
break;
case "number":
shape[field.name] = z.number();
break;
case "boolean":
shape[field.name] = z.boolean();
break;
}
if (field.optional) {
shape[field.name] = shape[field.name].optional();
}
}
return z.object(shape);
};
Такой подход используется в системах форм, CMS и административных панелях, где структура данных задаётся внешними описаниями.
Схемы могут комбинироваться как обычные функции.
const withTimestamp = (schema) =>
schema.extend({
createdAt: z.date(),
updatedAt: z.date(),
});
const withSoftDelete = (schema) =>
schema.extend({
deletedAt: z.date().nullable(),
});
const base = z.object({
id: z.string(),
});
const enhancedSchema = withSoftDelete(withTimestamp(base));
Такая композиция позволяет строить сложные модели без дублирования логики.
Zod поддерживает расширение схем:
const base = z.object({
id: z.string(),
});
const extended = base.extend({
name: z.string(),
});
Для более сложных случаев используется merge:
const schemaA = z.object({
id: z.string(),
});
const schemaB = z.object({
metadata: z.object({
created: z.date(),
}),
});
const merged = schemaA.merge(schemaB);
При динамическом формировании это позволяет собирать схемы из независимых модулей.
Фундаментальный паттерн динамических схем — возврат валидатора из функции:
const getSchemaByVersion = (version) => {
if (version === "v1") {
return z.object({
name: z.string(),
});
}
if (version === "v2") {
return z.object({
fullName: z.string(),
age: z.number(),
});
}
return z.object({
id: z.string(),
});
};
Такой подход используется при поддержке нескольких версий API одновременно.
В динамических схемах часто требуется изменять обязательность полей:
const createProfileSchema = (isStrict) =>
z.object({
username: z.string(),
email: isStrict ? z.string().email() : z.string().optional(),
});
Это позволяет адаптировать валидацию под режимы работы системы: строгий или мягкий.
В сложных приложениях схемы Zod становятся промежуточным слоем между данными и бизнес-логикой. Динамическая генерация превращает их в инструмент описания поведения системы.
Вместо фиксированных структур используются правила генерации, которые:
Такой подход позволяет держать валидацию ближе к реальности данных, а не к их формальному описанию на момент разработки.