Динамические схемы

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

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

import { z } from "zod";

const createUserSchema = (minAge) =>
  z.object({
    name: z.string(),
    age: z.number().min(minAge),
  });

const schema = createUserSchema(18);

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

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

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

const createPaymentSchema = (method) => {
  if (method === "card") {
    return z.object({
      method: z.literal("card"),
      cardNumber: z.string(),
      cvv: z.string(),
    });
  }

  if (method === "paypal") {
    return z.object({
      method: z.literal("paypal"),
      email: z.string().email(),
    });
  }

  return z.object({
    method: z.literal("cash"),
  });
};

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

Динамическая композиция объектов

Схемы объектов можно собирать программно, добавляя поля на основе конфигурации.

const baseSchema = {
  id: z.string(),
  createdAt: z.date(),
};

const withOptionalFields = (flags) => {
  const schemaShape = { ...baseSchema };

  if (flags.includeEmail) {
    schemaShape.email = z.string().email();
  }

  if (flags.includeRole) {
    schemaShape.role = z.enum(["admin", "user"]);
  }

  return z.object(schemaShape);
};

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

Использование z.lazy для рекурсивных структур

Динамические схемы часто пересекаются с рекурсивными структурами. Особенно это заметно в деревьях, комментариях, меню и графах.

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

z.lazy откладывает вычисление схемы до момента использования, позволяя ссылаться на саму себя.

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

Динамические массивы и вложенные схемы

Массивы могут содержать элементы, тип которых определяется во время выполнения.

const createArraySchema = (type) => {
  if (type === "string") return z.array(z.string());
  if (type === "number") return z.array(z.number());

  return z.array(z.any());
};

Более сложные сценарии используют комбинирование схем:

const createMixedArraySchema = (config) =>
  z.array(
    z.union([
      config.allowStrings && z.string(),
      config.allowNumbers && z.number(),
    ].filter(Boolean))
  );

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

Динамические ключи через z.record

Когда структура объекта неизвестна заранее, используется z.record. Это позволяет описывать словари с динамическими ключами.

const schema = z.record(z.string(), z.number());

В более сложных сценариях ключи могут зависеть от бизнес-логики:

const createSettingsSchema = (allowedKeys) =>
  z.record(
    z.enum(allowedKeys),
    z.union([z.string(), z.number(), z.boolean()])
  );

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

Динамические объединения через z.union

Объединения схем могут формироваться во время выполнения.

const createUnionSchema = (types) => {
  const schemas = [];

  if (types.includes("user")) {
    schemas.push(z.object({ type: z.literal("user"), name: z.string() }));
  }

  if (types.includes("admin")) {
    schemas.push(z.object({ type: z.literal("admin"), permissions: z.array(z.string()) }));
  }

  return z.union(schemas);
};

Это особенно полезно при обработке ответов API, где набор возможных вариантов расширяется со временем.

Discriminated unions в динамическом контексте

Когда у объектов есть дискриминатор (например, поле type), Zod позволяет строить более строгие структуры:

const createEventSchema = (enabledEvents) => {
  const schemas = [];

  if (enabledEvents.includes("click")) {
    schemas.push(
      z.object({
        type: z.literal("click"),
        x: z.number(),
        y: z.number(),
      })
    );
  }

  if (enabledEvents.includes("scroll")) {
    schemas.push(
      z.object({
        type: z.literal("scroll"),
        position: z.number(),
      })
    );
  }

  return z.discriminatedUnion("type", schemas);
};

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

Препроцессинг входных данных перед схемой

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

const schema = z.preprocess((input) => {
  if (typeof input === "string") {
    return JSON.parse(input);
  }
  return input;
}, z.object({
  id: z.string(),
}));

Этот механизм позволяет адаптировать данные из нестабильных источников: форм, API или внешних сервисов.

Генерация схем на основе конфигураций

В реальных приложениях схемы часто строятся из конфигурационных файлов или описаний API.

const buildSchemaFromConfig = (config) => {
  const shape = {};

  for (const field of config.fields) {
    switch (field.type) {
      case "string":
        shape[field.name] = z.string();
        break;
      case "number":
        shape[field.name] = z.number();
        break;
      case "boolean":
        shape[field.name] = z.boolean();
        break;
    }

    if (field.optional) {
      shape[field.name] = shape[field.name].optional();
    }
  }

  return z.object(shape);
};

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

Композиция схем через функции высшего порядка

Схемы могут комбинироваться как обычные функции.

const withTimestamp = (schema) =>
  schema.extend({
    createdAt: z.date(),
    updatedAt: z.date(),
  });

const withSoftDelete = (schema) =>
  schema.extend({
    deletedAt: z.date().nullable(),
  });

const base = z.object({
  id: z.string(),
});

const enhancedSchema = withSoftDelete(withTimestamp(base));

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

Динамическое расширение через extend и merge

Zod поддерживает расширение схем:

const base = z.object({
  id: z.string(),
});

const extended = base.extend({
  name: z.string(),
});

Для более сложных случаев используется merge:

const schemaA = z.object({
  id: z.string(),
});

const schemaB = z.object({
  metadata: z.object({
    created: z.date(),
  }),
});

const merged = schemaA.merge(schemaB);

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

Динамическая типизация через функции возврата схем

Фундаментальный паттерн динамических схем — возврат валидатора из функции:

const getSchemaByVersion = (version) => {
  if (version === "v1") {
    return z.object({
      name: z.string(),
    });
  }

  if (version === "v2") {
    return z.object({
      fullName: z.string(),
      age: z.number(),
    });
  }

  return z.object({
    id: z.string(),
  });
};

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

Условная обязательность полей

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

const createProfileSchema = (isStrict) =>
  z.object({
    username: z.string(),
    email: isStrict ? z.string().email() : z.string().optional(),
  });

Это позволяет адаптировать валидацию под режимы работы системы: строгий или мягкий.

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

В сложных приложениях схемы Zod становятся промежуточным слоем между данными и бизнес-логикой. Динамическая генерация превращает их в инструмент описания поведения системы.

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

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

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