Переиспользуемые валидаторы

Базовый принцип переиспользования схем

Одной из ключевых идей при работе с Zod является построение валидаторов как композиции небольших, независимых и повторно используемых блоков. Схемы перестают быть одноразовыми описаниями структуры данных и превращаются в доменные строительные элементы.

Переиспользуемый валидатор — это схема или функция, возвращающая схему, которая используется в разных частях приложения без дублирования логики валидации.

Основные цели:

  • устранение дублирования правил
  • единообразие бизнес-ограничений
  • централизованное управление изменениями
  • повышение читаемости схем

Базовые примитивы как строительные блоки

В Zod каждый примитив может стать частью общей библиотеки валидаторов:

import { z } from "zod";

export const email = z.string().email();
export const password = z.string().min(8).max(64);
export const uuid = z.string().uuid();

Такие определения становятся фундаментом для более сложных схем.

Использование:

const userSchema = z.object({
  email,
  password,
});

Константы ограничений как источник единой правды

Частая проблема — расхождение ограничений между различными схемами. Решение — вынесение параметров в отдельные константы:

const PASSWORD_MIN = 8;
const PASSWORD_MAX = 64;

export const password = z.string()
  .min(PASSWORD_MIN)
  .max(PASSWORD_MAX);

Преимущество подхода:

  • изменение правил происходит в одном месте
  • исключается рассинхронизация логики

Фабрики схем (schema factories)

Когда валидация зависит от параметров, используются функции, возвращающие схемы.

export const minMaxString = (min: number, max: number) =>
  z.string().min(min).max(max);

Применение:

const nickname = minMaxString(3, 20);
const title = minMaxString(5, 100);

Фабрики особенно полезны для:

  • строковых ограничений
  • числовых диапазонов
  • массивов с ограничением длины

Композиция через extend и merge

Переиспользование часто строится через расширение существующих схем.

extend

const baseUser = z.object({
  id: z.string().uuid(),
  createdAt: z.date(),
});

const fullUser = baseUser.extend({
  email: z.string().email(),
});

merge

const authFields = z.object({
  email: z.string().email(),
  password: z.string(),
});

const profileFields = z.object({
  name: z.string(),
});

const user = authFields.merge(profileFields);

Разница:

  • extend добавляет поля к одной схеме
  • merge объединяет две равноправные схемы

Переиспользуемые refine-валидаторы

Бизнес-правила часто повторяются и могут быть вынесены в функции.

const noSpaces = (value: string) => !value.includes(" ");

Использование:

export const username = z.string().refine(noSpaces, {
  message: "Строка не должна содержать пробелы",
});

Более универсальный вариант:

export const createNoSpacesValidator = (message: string) =>
  z.string().refine((val) => !val.includes(" "), { message });

Преобразование входных данных через preprocess

Переиспользуемые трансформеры позволяют унифицировать входные данные.

export const toNumber = z.preprocess((val) => {
  if (typeof val === "string") return Number(val);
  return val;
}, z.number());

Использование:

const age = toNumber;

Подходит для:

  • строковых чисел
  • нормализации форматов API
  • обработки legacy-данных

Ленивая и рекурсивная переиспользуемость

Для структур с самоссылками применяется z.lazy:

const category: z.ZodType<any> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(category).optional(),
  })
);

Такой подход позволяет:

  • строить деревья
  • описывать графы
  • избегать циклических зависимостей

Доменные валидаторы как слой абстракции

В зрелых проектах создаётся слой доменных валидаторов.

Пример структуры:

validators/
  primitives/
    email.ts
    uuid.ts
  domain/
    user.ts
    product.ts
  factories/
    stringRange.ts

Пример доменного валидатора:

import { email, password } from "../primitives";

export const loginSchema = z.object({
  email,
  password,
});

Переиспользование через partial, pick и omit

Часто требуется извлекать части схем:

const user = z.object({
  id: z.string(),
  email: z.string(),
  password: z.string(),
});

pick

const publicUser = user.pick({
  id: true,
  email: true,
});

omit

const safeUser = user.omit({
  password: true,
});

partial

const updateUser = user.partial();

Эти методы создают новые переиспользуемые формы одной модели.


Брендированные типы как усиление переиспользования

Для различения идентификаторов используется branding:

const UserId = z.string().uuid().brand<"UserId">();
const OrderId = z.string().uuid().brand<"OrderId">();

Преимущества:

  • исключение путаницы типов
  • строгая типизация доменных сущностей
  • повторное использование базовых схем с семантическим различием

Глобальные утилиты для схем

Часто создаются универсальные обёртки:

export const requiredString = z.string().min(1);

export const optionalString = z.string().optional();

export const positiveNumber = z.number().positive();

Эти утилиты формируют единый стиль валидации по всему проекту.


Централизация сообщений об ошибках

Переиспользуемые валидаторы часто включают стандартизированные сообщения:

export const email = z.string().email({
  message: "Некорректный email",
});

Или через фабрики:

export const requiredField = (field: string) =>
  z.string().min(1, `${field} является обязательным`);

Тестируемость переиспользуемых валидаторов

Модульный подход позволяет тестировать валидаторы отдельно от бизнес-логики:

test("email validator", () => {
  expect(email.safeParse("test@mail.com").success).toBe(true);
  expect(email.safeParse("invalid").success).toBe(false);
});

Изоляция схем повышает стабильность всей системы валидации.


Архитектурный эффект переиспользуемых схем

Систематическое применение переиспользуемых валидаторов приводит к:

  • уменьшению количества дублируемого кода
  • повышению консистентности данных
  • упрощению масштабирования схем
  • снижению стоимости изменений бизнес-правил
  • формированию устойчивой доменной модели