Кастомные строковые валидаторы

Валидация строк в Zod строится вокруг базового типа z.string(), к которому последовательно добавляются ограничения и пользовательские правила. Помимо встроенных методов проверки длины, формата и регулярных выражений, библиотека предоставляет механизмы для создания полностью кастомной логики через refine, superRefine и композицию функций, что позволяет описывать сложные доменные ограничения без выхода за пределы схемы.

Строковый тип формируется через z.string() и может быть расширен стандартными ограничителями:

import { z } from "zod";

const schema = z.string()
  .min(5)
  .max(20)
  .trim();

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

Кастомная валидация через refine

Метод refine используется для добавления произвольного условия, возвращающего булево значение. При возврате false формируется ошибка валидации.

const usernameSchema = z.string().refine((value) => {
  return /^[a-zA-Z0-9_]+$/.test(value);
}, {
  message: "Недопустимые символы в строке"
});

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

Ключевая особенность refine заключается в том, что он не предоставляет контекста пути ошибки, ограничиваясь одной причиной сбоя.

Сложная логика через superRefine

Метод superRefine применяется при необходимости формировать несколько ошибок или анализировать строку более детально. В отличие от refine, он предоставляет объект контекста, через который добавляются ошибки с указанием пути и кода.

const passwordSchema = z.string().superRefine((value, ctx) => {
  if (value.length < 8) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_small,
      minimum: 8,
      type: "string",
      inclusive: true,
      message: "Слишком короткая строка"
    });
  }

  if (!/[A-Z]/.test(value)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Отсутствует заглавная буква"
    });
  }

  if (!/[0-9]/.test(value)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Отсутствует цифра"
    });
  }
});

Такая модель позволяет аккумулировать ошибки в одной проверке без преждевременного завершения валидации.

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

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

const startsWith = (prefix) =>
  z.string().refine((value) => value.startsWith(prefix), {
    message: `Строка должна начинаться с ${prefix}`
  });

const uppercaseOnly = () =>
  z.string().refine((value) => value === value.toUpperCase(), {
    message: "Допустимы только заглавные буквы"
  });

Такой подход превращает валидацию в набор строительных блоков, которые можно комбинировать через .and() или последовательные вызовы.

Валидация форматов: email, URL, slug

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

const slugSchema = z.string().refine((value) => {
  return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
}, {
  message: "Некорректный slug"
});

Email и URL часто требуют более строгих или ограниченных проверок, чем встроенные реализации, особенно в доменных приложениях:

const domainEmail = z.string().refine((value) => {
  return value.endsWith("@example.com");
}, {
  message: "Разрешены только корпоративные адреса"
});

Регулярные выражения и refine

Регулярные выражения остаются наиболее компактным способом описания формата строки, но их использование внутри Zod требует баланса между читаемостью и поддерживаемостью.

const phoneSchema = z.string().refine((value) => {
  return /^\+?[0-9]{10,15}$/.test(value);
});

Однако сложные регулярные выражения часто заменяются цепочкой проверок в superRefine, что улучшает диагностику ошибок и читаемость кода.

Модификация строк перед валидацией

В Zod возможно преобразование строки до проверки через transform или preprocess. Это особенно важно при нормализации данных.

const normalizedSchema = z.string()
  .transform((value) => value.trim().toLowerCase())
  .refine((value) => value.length > 0, {
    message: "Пустая строка недопустима"
  });

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

Асинхронные кастомные валидаторы

При необходимости проверки внешних данных используется асинхронный refine. Это актуально для проверки уникальности или обращения к API.

const uniqueUsername = z.string().refine(async (value) => {
  const res = await fetch(`/api/check?username=${value}`);
  const data = await res.json();
  return data.available;
}, {
  message: "Имя пользователя уже занято"
});

Асинхронные проверки требуют осторожности, поскольку влияют на производительность и характер выполнения схемы.

Композиция нескольких правил

Кастомные строковые валидаторы часто комбинируются с базовыми ограничениями Zod, образуя цепочки проверок:

const schema = z.string()
  .min(3)
  .max(30)
  .trim()
  .refine((value) => /^[a-z0-9_]+$/.test(value))
  .refine((value) => !value.includes("__"), {
    message: "Недопустимая последовательность символов"
  });

Каждое правило добавляет отдельный уровень проверки, сохраняя читаемость за счёт декларативного подхода.

Контекстные ошибки и детализация

Использование superRefine позволяет формировать структурированные ошибки, привязанные к конкретным условиям строки. Это особенно важно при сложной бизнес-валидации, где одна строка может нарушать несколько независимых правил одновременно.

const codeSchema = z.string().superRefine((value, ctx) => {
  if (!value.startsWith("ID-")) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Отсутствует префикс ID-"
    });
  }

  if (value.length !== 10) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Неверная длина кода"
    });
  }
});

Частые паттерны нормализации строк

В практических схемах часто применяется предварительная очистка данных перед проверкой:

const cleaned = z.string()
  .transform((v) => v.replace(/\s+/g, " ").trim())
  .refine((v) => v.length > 0);

Подобные преобразования уменьшают количество ложных ошибок и стандартизируют входные данные.

Ограничения и особенности поведения

Кастомные строковые валидаторы выполняются после базовых проверок типа. Это означает, что refine и superRefine не вызываются, если значение не является строкой. Такой порядок важен при работе с неизвестными входными данными.

Дополнительно стоит учитывать, что каждая цепочка refine добавляет отдельный этап выполнения, что может влиять на производительность при большом количестве правил или при использовании асинхронных проверок.

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