Ленивая валидация

Понятие ленивой валидации

Ленивая валидация в контексте Zod — это способ отложенного определения схемы до момента её фактического использования. В стандартном сценарии Zod требует, чтобы схема была полностью определена в момент объявления. Однако при работе с рекурсивными структурами, взаимными зависимостями или динамически формируемыми типами возникает необходимость откладывать вычисление схемы.

Основной механизм, реализующий ленивую валидацию, — z.lazy.


Механизм z.lazy

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

import { z } from "zod";

const schema = z.lazy(() => z.string());

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

Функция внутри z.lazy не выполняется сразу при объявлении, что позволяет ссылаться на ещё не определённые структуры.


Рекурсивные структуры

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

Пример дерева

import { z } from "zod";

const Category = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(Category).optional(),
  })
);

Здесь Category ссылается на саму себя. Без z.lazy такое определение невозможно, так как переменная ещё не инициализирована в момент её использования.


Рекурсивные списки и графы

Ленивая валидация используется не только для деревьев, но и для более сложных структур:

  • вложенные категории
  • комментарии с ответами
  • графы зависимостей
  • древовидные меню
const Comment = z.lazy(() =>
  z.object({
    id: z.number(),
    text: z.string(),
    replies: z.array(Comment).default([]),
  })
);

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


Взаимные ссылки между схемами

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

const User = z.lazy(() =>
  z.object({
    name: z.string(),
    posts: z.array(Post),
  })
);

const Post = z.lazy(() =>
  z.object({
    title: z.string(),
    author: User,
  })
);

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


Особенности выполнения

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

Важно учитывать:

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

Производительность и поведение

Ленивая валидация добавляет минимальные накладные расходы, связанные с вызовом функции. Однако основной эффект проявляется не в замедлении, а в архитектурной гибкости.

Особенности:

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

Сочетание с другими типами Zod

z.lazy может комбинироваться с любыми типами Zod:

Объекты

const Node = z.lazy(() =>
  z.object({
    value: z.number(),
    next: Node.optional(),
  })
);

Массивы

const Tree = z.lazy(() =>
  z.array(
    z.object({
      id: z.string(),
      children: Tree.optional(),
    })
  )
);

Union-типы

const Expr = z.lazy(() =>
  z.union([
    z.object({ type: z.literal("num"), value: z.number() }),
    z.object({
      type: z.literal("add"),
      left: Expr,
      right: Expr,
    }),
  ])
);

Ограничения ленивой валидации

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

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

Типовые ошибки

Бесконечная рекурсия

const A = z.lazy(() => A);

Такое определение приводит к бесконечному циклу вызовов.


Несогласованные схемы

const A = z.lazy(() => z.string());
const B = z.lazy(() => z.number());

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


Практическая модель применения

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

  • CMS-системы с вложенными страницами
  • комментарии и ответы
  • организационные структуры
  • конфигурации с зависимостями
  • AST (абстрактные синтаксические деревья)

Разрешение циклических зависимостей

Ключевая задача z.lazy — устранение проблемы циклического определения типов на уровне исполнения JavaScript.

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


Поведение при валидации

При вызове .parse() или .safeParse():

  1. вызывается функция внутри z.lazy
  2. возвращается актуальная схема
  3. выполняется стандартная валидация Zod
  4. рекурсивные структуры обрабатываются по мере необходимости

Композиция с трансформациями

Ленивая валидация может использоваться совместно с .transform():

const Node = z.lazy(() =>
  z.object({
    value: z.number(),
    next: Node.optional(),
  }).transform((data) => ({
    ...data,
    hasNext: !!data.next,
  }))
);

Использование в динамических схемах

z.lazy позволяет формировать схемы на основе внешнего состояния:

const createSchema = (strict) =>
  z.lazy(() =>
    z.object({
      id: z.number(),
      meta: strict ? z.object({}).strict() : z.object({}),
    })
  );

Такой подход используется при построении конфигурируемых валидаторов.


Архитектурное значение

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

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