Параллельная валидация в контексте Zod строится вокруг идеи одновременного выполнения независимых проверок данных, особенно когда часть логики требует обращения к внешним источникам: базам данных, API, кэшу или вычислительно дорогим операциям. В отличие от синхронной модели, где каждая проверка выполняется последовательно, параллельный подход позволяет значительно снизить латентность валидации сложных объектов.
Zod изначально проектировался как синхронная библиотека схем, однако поддержка асинхронных операций реализована через отдельный набор методов.
Синхронная валидация:
const schema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
schema.parse(data);
Асинхронная валидация:
await schema.parseAsync(data);
или
await schema.safeParseAsync(data);
Асинхронный режим становится обязательным, как только в схему
добавляются проверки, возвращающие Promise.
Zod поддерживает асинхронные проверки через refine и
superRefine.
const schema = z.object({
email: z.string().email().refine(async (email) => {
const exists = await checkEmailInDatabase(email);
return !exists;
}, {
message: "Email уже используется",
}),
});
Проблема такого подхода проявляется при нескольких независимых
проверках: каждая await внутри refine
выполняется последовательно, если они не объединены.
Основной способ достижения параллельной валидации заключается в явном
использовании Promise.all.
const schema = z.object({
email: z.string().email(),
username: z.string(),
}).superRefine(async (data, ctx) => {
const emailCheck = checkEmailInDatabase(data.email);
const usernameCheck = checkUsernameInDatabase(data.username);
const [emailExists, usernameExists] = await Promise.all([
emailCheck,
usernameCheck,
]);
if (emailExists) {
ctx.addIssue({
path: ["email"],
code: "custom",
message: "Email уже используется",
});
}
if (usernameExists) {
ctx.addIssue({
path: ["username"],
code: "custom",
message: "Username уже занят",
});
}
});
Такой подход устраняет последовательное ожидание и сводит время валидации к максимальному времени самого медленного запроса, а не сумме всех.
Параллельная модель особенно важна при наличии независимых проверок:
Каждая из этих операций может выполняться независимо, что делает последовательную модель неоптимальной.
Антипаттерн возникает при следующей структуре:
superRefine(async (data, ctx) => {
const emailExists = await checkEmail(data.email);
const usernameExists = await checkUsername(data.username);
});
Здесь проверки выполняются строго одна за другой. При увеличении числа проверок задержка растёт линейно.
Эффективная стратегия заключается в выносе независимых операций до этапа валидации или их группировке:
async function validateUser(data) {
const checks = await Promise.all([
checkEmail(data.email),
checkUsername(data.username),
checkPhone(data.phone),
]);
return schema.parseAsync({
...data,
__checks: checks,
});
}
Далее результаты используются внутри superRefine без
дополнительных await.
При массовой обработке данных важно сохранять устойчивость к ошибкам отдельных элементов:
const results = await Promise.all(
users.map(user => schema.safeParseAsync(user))
);
Каждая валидация выполняется независимо, а общий поток не блокируется на первом неуспешном результате.
При необходимости получения полного набора ошибок используется:
const results = await Promise.allSettled([
schema.parseAsync(data1),
schema.parseAsync(data2),
]);
Это позволяет обрабатывать как успешные, так и ошибочные результаты без прерывания выполнения.
При большом количестве одновременных проверок (например, массовая валидация тысяч объектов) требуется контроль нагрузки:
import pLimit fr om "p-lim it";
const limit = pLimit(10);
const results = await Promise.all(
items.map(item =>
limit(() => schema.parseAsync(item))
)
);
Ограничение параллелизма предотвращает перегрузку базы данных и API.
При работе со вложенными структурами параллелизм сохраняется на уровне независимых ветвей:
const schema = z.object({
profile: z.object({
email: z.string().email(),
phone: z.string(),
}),
settings: z.object({
theme: z.string(),
}),
}).superRefine(async (data, ctx) => {
const [emailTaken, phoneValid] = await Promise.all([
checkEmail(data.profile.email),
validatePhone(data.profile.phone),
]);
if (emailTaken) {
ctx.addIssue({
path: ["profile", "email"],
message: "Email занят",
code: "custom",
});
}
if (!phoneValid) {
ctx.addIssue({
path: ["profile", "phone"],
message: "Некорректный номер",
code: "custom",
});
}
});
Zod не прекращает выполнение всех async-операций при первой ошибке
внутри superRefine, если они уже запущены через
Promise.all. Это делает важным контроль количества и
стоимости запусков до их старта.
При повторяющихся запросах к одним и тем же данным параллельные проверки могут использовать кэш:
const cache = new Map();
function cachedCheckEmail(email) {
if (cache.has(email)) return cache.get(email);
const promise = checkEmailInDatabase(email);
cache.set(email, promise);
return promise;
}
Это особенно эффективно при массовой валидации списков пользователей.
При использовании z.union или
z.discriminatedUnion каждая ветка может содержать
собственные асинхронные проверки. Однако важно учитывать, что Zod
проверяет варианты последовательно до нахождения подходящего.
const schema = z.union([
z.object({
type: z.literal("A"),
value: z.string().refine(async (v) => checkA(v)),
}),
z.object({
type: z.literal("B"),
value: z.string().refine(async (v) => checkB(v)),
}),
]);
Оптимизация достигается путем предварительного дискриминирования, чтобы избежать лишних async-вызовов.
В Node.js-средах параллельная валидация часто становится частью request pipeline:
await Promise.all([
validateToken(req.headers.authorization),
schema.parseAsync(req.body),
]);
Такой подход снижает общий latency запроса без усложнения архитектуры.
Несмотря на эффективность, параллельная валидация требует контроля следующих аспектов:
Эти ограничения напрямую влияют на архитектуру схем и стратегию их композиции.