Метод schema для динамического определения

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

Принцип динамической схемы

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

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

import * as yup from 'yup';

const schema = yup.lazy((value) => {
  if (typeof value === 'string') {
    return yup.string().min(3).max(20);
  }

  if (typeof value === 'number') {
    return yup.number().min(0).max(100);
  }

  return yup.mixed().notRequired();
});

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

Динамическая схема объектов

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

const userSchema = yup.object().shape({
  type: yup.string().required(),
}).when('type', (type, schema) => {
  if (type === 'admin') {
    return schema.shape({
      permissions: yup.array().of(yup.string()).required(),
      accessLevel: yup.number().min(1).max(10),
    });
  }

  if (type === 'guest') {
    return schema.shape({
      expirationDate: yup.date().required(),
    });
  }

  return schema;
});

Метод when позволяет изменять схему на основе значения одного или нескольких полей, создавая условную структуру объекта.

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

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

const nodeSchema = yup.lazy((value) => {
  if (Array.isArray(value)) {
    return yup.array().of(nodeSchema);
  }

  if (typeof value === 'object' && value !== null) {
    return yup.object({
      name: yup.string().required(),
      children: yup.array().of(nodeSchema).notRequired(),
    });
  }

  return yup.mixed().oneOf([null]);
});

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

Комбинирование lazy и when

В сложных сценариях часто используется комбинация lazy и when. Это позволяет строить гибридные схемы, где часть логики зависит от значения, а часть — от типа данных.

const schema = yup.object({
  data: yup.lazy((value) => {
    if (value && value.kind === 'numeric') {
      return yup.object({
        kind: yup.string().required(),
        value: yup.number().required().min(0),
      });
    }

    if (value && value.kind === 'text') {
      return yup.object({
        kind: yup.string().required(),
        value: yup.string().required().min(1),
      });
    }

    return yup.mixed().notRequired();
  }),
});

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

Динамическое формирование массивов

При работе с массивами динамическая схема может зависеть от содержимого каждого элемента или внешнего контекста.

const schema = yup.array().of(
  yup.lazy((value) => {
    if (value && value.role === 'admin') {
      return yup.object({
        role: yup.string().required(),
        permissions: yup.array().of(yup.string()).required(),
      });
    }

    return yup.object({
      role: yup.string().required(),
      active: yup.boolean(),
    });
  })
);

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

Контекстная динамика через parent и options

Yup предоставляет доступ к контексту через второй аргумент функции в lazy, что позволяет учитывать не только текущее значение, но и внешние данные.

const schema = yup.lazy((value, options) => {
  const parent = options?.parent;

  if (parent?.isStrictMode) {
    return yup.string().required().strict();
  }

  return yup.string().nullable();
});

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

Типовые ошибки при динамическом определении схем

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

Одна из распространённых проблем — несоответствие типов:

const schema = yup.lazy((value) => {
  if (value.flag) {
    return yup.number();
  }

  return yup.string();
});

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

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

Производительность динамических схем

Динамическое определение схемы увеличивает количество вычислений во время валидации. Каждое обращение к lazy или when приводит к повторной оценке условий.

Для снижения нагрузки применяются следующие подходы:

  • минимизация вложенных lazy вызовов
  • предварительная нормализация данных
  • выделение повторяющихся схем в отдельные константы
  • использование статических схем там, где это возможно

Композиция динамических схем

Динамические схемы можно комбинировать через concat, создавая сложные модульные структуры.

const baseSchema = yup.object({
  id: yup.string().required(),
});

const extendedSchema = yup.lazy((value) => {
  if (value?.type === 'full') {
    return baseSchema.concat(
      yup.object({
        details: yup.string().required(),
      })
    );
  }

  return baseSchema;
});

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

Рекурсивная динамика и самоописание схем

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

const categorySchema = yup.lazy(() =>
  yup.object({
    name: yup.string().required(),
    children: yup.array().of(categorySchema).notRequired(),
  })
);

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

Особенности работы с null и undefined

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

const schema = yup.lazy((value) => {
  if (value == null) {
    return yup.mixed().notRequired();
  }

  return yup.string().required();
});

Игнорирование этого случая часто приводит к неожиданным ошибкам валидации.

Итеративное построение схем

В некоторых архитектурах схемы формируются поэтапно, где каждый слой добавляет новую логику.

let schema = yup.object({
  base: yup.string().required(),
});

if (process.env.NODE_ENV === 'development') {
  schema = schema.shape({
    debug: yup.boolean(),
  });
}

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