Валидация данных в Zod начинается не всегда с «чистого» входного
значения. Часто вход поступает из внешних источников в неудобном или
нестабильном формате: строки вместо чисел, null вместо
объектов, JSON с неожиданными типами. Для нормализации таких случаев
используется механизм предварительной обработки —
preprocess.
preprocess выполняет функцию преобразования входного
значения до того, как оно попадёт в основную схему валидации.
Базовая форма:
import { z } from "zod";
const schema = z.preprocess((input) => {
return input;
}, z.string());
Функция в preprocess получает исходное значение без
проверок типов. Результат этой функции передаётся дальше в
валидатор.
Типичный сценарий — приведение строк к числам:
const numberSchema = z.preprocess((input) => {
if (typeof input === "string") {
const parsed = Number(input);
return isNaN(parsed) ? input : parsed;
}
return input;
}, z.number());
Здесь вход "42" становится числом 42, а
некорректные строки передаются дальше, где уже срабатывает ошибка
валидации z.number().
Данные из API или форм часто содержат null или
undefined, которые требуется заменить на значения по
умолчанию:
const schema = z.preprocess((input) => {
if (input == null) return 0;
return input;
}, z.number());
Такой подход позволяет централизованно устранять неопределённые значения до валидации.
Часто требуется нормализация строк: удаление пробелов, приведение к одному регистру:
const schema = z.preprocess((input) => {
if (typeof input === "string") {
return input.trim().toLowerCase();
}
return input;
}, z.string());
Подобные преобразования полезны для email, логинов, ключей и идентификаторов.
В отличие от preprocess, который работает до
валидации, transform применяется после успешной
проверки типа. Это принципиально различает их назначение:
transform предполагает, что данные уже валидны.
Базовая форма:
const schema = z.string().transform((value) => {
return value.length;
});
В этом примере строка превращается в число — её длину.
transform позволяет менять тип результата схемы:
const schema = z.string().transform((str) => Number(str));
На входе остаётся строка, но на выходе получается число. Тип
результата схемы становится number.
Ключевое различие заключается в моменте выполнения:
preprocess — до проверки типаtransform — после успешной проверкиЭта разница влияет на поведение при ошибках.
Пример:
const schema = z.preprocess((val) => val, z.number());
Если передать "abc", преобразование не выполнится, и
ошибка возникнет на этапе z.number().
const schema = z.number().transform((val) => val * 2);
Если передать "abc", ошибка возникнет сразу, так как
строка не проходит проверку z.number(), и
transform не будет вызван.
На практике оба механизма часто используются вместе:
const schema = z.preprocess((input) => {
if (typeof input === "string") {
return input.trim();
}
return input;
}, z.string().transform((str) => str.length));
Здесь происходит двухэтапная обработка:
preprocess очищает входtransform вычисляет длину строкиZod позволяет строить цепочки трансформаций, создавая конвейер обработки данных.
const schema = z
.string()
.transform((str) => str.trim())
.transform((str) => str.toUpperCase())
.transform((str) => str.split(" "));
Результат такого пайплайна — массив слов в верхнем регистре.
Каждый transform получает результат предыдущего.
Каждый transform изменяет тип схемы. Это важно при
проектировании цепочек:
const schema = z.string().transform((s) => Number(s)).transform((n) => n > 10);
Итоговый тип — boolean.
Таким образом, трансформации формируют не только значение, но и типовую структуру данных.
Объекты позволяют выполнять более сложные преобразования:
const schema = z.object({
firstName: z.string(),
lastName: z.string(),
}).transform((data) => {
return {
fullName: `${data.firstName} ${data.lastName}`
};
});
Здесь входной объект заменяется на новый с вычисленным полем.
Иногда требуется модифицировать объект, сохраняя часть данных:
const schema = z.object({
name: z.string(),
age: z.number(),
}).transform((data) => ({
...data,
isAdult: data.age >= 18
}));
Результат содержит исходные поля и вычисленное свойство.
Zod поддерживает асинхронные преобразования, если используется
async контекст:
const schema = z.string().transform(async (id) => {
const response = await fetch(`/api/user/${id}`);
return response.json();
});
Такой подход превращает схему в асинхронную, и результатом становится
Promise.
Важный аспект: после transform тип данных меняется, но
дополнительные проверки могут быть добавлены через
refine.
const schema = z
.string()
.transform((s) => Number(s))
.refine((n) => n > 0, {
message: "Число должно быть положительным"
});
Здесь сначала выполняется преобразование, затем проверка результата.
В реальных приложениях входные данные часто приходят в виде строк JSON:
const schema = z.preprocess((input) => {
if (typeof input === "string") {
try {
return JSON.parse(input);
} catch {
return input;
}
}
return input;
}, z.object({
id: z.number(),
name: z.string()
}));
Такой подход позволяет обрабатывать данные, поступающие из форм или внешних API без предварительного парсинга.
Ошибки часто возникают при неосторожной трансформации типов:
z.string().transform((val) => val.length)
Если вход не строка, ошибка возникает до трансформации. Поэтому важно учитывать порядок:
preprocess — для приведения типовtransform — для изменения уже валидных данныхТипичный сценарий — построение доменных объектов:
const userSchema = z.object({
firstName: z.string(),
lastName: z.string(),
birthYear: z.number()
}).transform((u) => ({
fullName: `${u.firstName} ${u.lastName}`,
age: new Date().getFullYear() - u.birthYear
}));
Схема не только валидирует данные, но и формирует готовую модель.
Внутренне обработка в Zod при использовании preprocess и
transform следует последовательной модели:
preprocesstransformКаждый этап влияет на следующий, формируя цепочку преобразований.
В TypeScript трансформации влияют на тип результата схемы:
const schema = z.string().transform((s) => Number(s));
// результат: ZodEffects<ZodString, number>
Это означает, что исходный тип string заменяется на
number в результате парсинга.
Трансформации не предназначены для сложной бизнес-логики с побочными эффектами. Основные ограничения:
transformpreprocess используется для адаптации входа к ожидаемой
структуре. transform используется для преобразования
валидированных данных в конечную форму.
Совместное использование формирует устойчивый конвейер обработки данных, в котором вход нормализуется, проверяется и преобразуется в доменную модель без промежуточных внешних шагов.