z.custom
Сигнатура:
z.custom<T>(
(value) => boolean,
params?: {
message?: string;
path?: string[];
}
)
Первый аргумент — функция-предикат, принимающая значение неизвестного
типа и возвращающая true при успешной валидации или
false при ошибке.
Простейшая схема:
import { z } from "zod";
const PositiveNumber = z.custom<number>((val) => {
return typeof val === "number" && val > 0;
});
В данном случае тип number указан явно через generic
<number>, но фактическая проверка полностью
контролируется функцией.
Генерик <T> не выполняет проверку во время
выполнения, а лишь сообщает системе типов, каким должен считаться
результат после успешной валидации.
const UserId = z.custom<string>((val) => {
return typeof val === "string" && val.length > 0;
});
type UserId = z.infer<typeof UserId>;
Результирующий тип будет string, несмотря на то, что
входное значение проходит произвольную проверку.
Часто используется сужение через пользовательские type guards:
function isUserId(val: unknown): val is string {
return typeof val === "string" && /^[a-z0-9_]+$/.test(val);
}
const UserId = z.custom<string>((val) => isUserId(val));
Такой подход позволяет связать runtime-проверку с TypeScript narrowing, но Zod не извлекает автоматически тип из type guard — он задаётся явно.
Параметр params.message задаёт статическое
сообщение:
const EvenNumber = z.custom<number>((val) => {
return typeof val === "number" && val % 2 === 0;
}, {
message: "Ожидается чётное число"
});
При необходимости динамического сообщения используется функция:
const Schema = z.custom<number>((val) => {
return typeof val === "number" && val > 10;
}, {
message: (issue) => `Недопустимое значение: ${String(issue.path)}`
});
z.custom часто применяется для проверки данных, которые невозможно выразить через стандартные схемы:
type Config = {
mode: "dev" | "prod";
flags: Record<string, boolean>;
};
const ConfigSchema = z.custom<Config>((val) => {
if (typeof val !== "object" || val === null) return false;
const obj = val as any;
if (obj.mode !== "dev" && obj.mode !== "prod") return false;
if (typeof obj.flags !== "object") return false;
return Object.values(obj.flags).every(v => typeof v === "boolean");
});
Такой подход полезен, когда структура динамическая или поступает из внешнего источника без строгого контракта.
Использование полностью пользовательской логики отключает ряд преимуществ Zod:
Поэтому z.custom рассматривается как инструмент последнего уровня абстракции.
Вместо z.custom часто используется refine, когда уже
есть базовая схема:
const Age = z.number().refine((val) => val >= 18, {
message: "Возраст должен быть не менее 18"
});
Разница заключается в том, что refine работает поверх
существующего парсинга типа, тогда как z.custom полностью
берёт контроль над проверкой.
Часто z.custom используется для преобразования unknown в
строго типизированный объект:
const ParsedJson = z.custom<Record<string, unknown>>((val) => {
return typeof val === "object" && val !== null;
});
Это особенно полезно при обработке данных после
JSON.parse, где исходный тип — any.
z.custom может использоваться для создания “брендированных” типов:
type Email = string & { __brand: "email" };
const EmailSchema = z.custom<Email>((val) => {
return typeof val === "string" && val.includes("@");
});
Хотя Zod не добавляет брендинг автоматически, он позволяет сохранить типовую семантику на уровне TypeScript.
При необходимости можно выбрасывать исключение внутри предиката:
const StrictSchema = z.custom<number>((val) => {
if (typeof val !== "number") {
throw new Error("Требуется число");
}
return val > 0;
});
Однако такой подход нарушает стандартную модель возврата
boolean и используется редко, так как усложняет обработку
ошибок Zod.
z.custom особенно полезен при работе с API-ответами, где структура не гарантирована:
const ApiResponse = z.custom<any>((val) => {
return (
typeof val === "object" &&
val !== null &&
"status" in val &&
"data" in val
);
});
После первичной проверки можно применять более строгие схемы к вложенным полям.
Функции-предикаты могут быть вынесены отдельно:
const isTimestamp = (val: unknown): val is number => {
return typeof val === "number" && val > 0;
};
const TimestampSchema = z.custom<number>((val) => isTimestamp(val));
Такой подход позволяет отделять бизнес-логику от описания схем.
z.custom может использоваться для проверки сложных массивов:
const MatrixSchema = z.custom<number[][]>((val) => {
if (!Array.isArray(val)) return false;
return val.every(
row => Array.isArray(row) && row.every(cell => typeof cell === "number")
);
});
При этом Zod не выполняет рекурсивную проверку автоматически — вся логика реализуется вручную.
z.custom часто комбинируется с transform для
нормализации данных:
const Normalized = z
.custom<string>((val) => typeof val === "string")
.transform((val) => val.trim().toLowerCase());
Такой подход позволяет сначала выполнить проверку, затем преобразование.
Сложные условия также реализуются вручную:
const FlagSchema = z.custom<{ enabled: boolean; value?: number }>((val) => {
if (typeof val !== "object" || val === null) return false;
const obj = val as any;
if (obj.enabled === true) {
return typeof obj.value === "number";
}
return true;
});
Подобные схемы невозможно выразить через базовые примитивы без потери читаемости.
При возврате false Zod генерирует стандартную ошибку
валидации. При этом структура ошибки менее информативна по сравнению с
z.object или z.union, так как отсутствует
контекст поля.
Для повышения диагностичности используется path:
z.custom<number>((val) => val > 0, {
path: ["positiveNumber"]
});
z.custom часто выступает резервным механизмом для случаев, когда схема слишком динамична:
В таких сценариях схема выступает как фильтр первого уровня, после которого данные проходят дальнейшую обработку.
Чрезмерное использование z.custom приводит к потере преимуществ декларативной модели. Схема превращается в набор императивных проверок, что снижает читаемость и усложняет сопровождение.
Оптимальная архитектура обычно комбинирует:
z.object, z.string, z.number
для структурных частейrefine для локальных условийz.custom для недекларируемых областейТакой баланс сохраняет типобезопасность и управляемость логики валидации.