Циклические зависимости в схемах валидации возникают тогда, когда две или более сущности ссылаются друг на друга напрямую или через цепочку промежуточных ссылок. В контексте Yup это особенно критично, поскольку схема валидации часто строится как статическая структура, ожидающая полной определённости всех типов на этапе инициализации.
На уровне логики это проявляется как ситуация, в которой объект A требует валидации объекта B, а объект B — объекта A. При попытке немедленного вычисления таких схем происходит либо бесконечная рекурсия, либо ошибка переполнения стека, либо частично инициализированная структура, приводящая к некорректной валидации.
Yup строит схемы декларативно, но вычисление некоторых узлов происходит лениво только в ограниченном числе случаев. Если схема описана напрямую через объектные ссылки, JavaScript выполняет её сразу при импорте модуля или создании объекта схемы.
Типичный источник проблемы — взаимные ссылки в объектах:
import * as yup from 'yup';
const schemaA = yup.object({
b: schemaB
});
const schemaB = yup.object({
a: schemaA
});
В этом примере происходит попытка использования schemaB
до её инициализации. Даже если порядок объявлений изменить, логическая
цикличность сохранится.
В результате:
ReferenceErrorОсновной механизм разрыва циклов в Yup — использование
yup.lazy. Эта функция позволяет отложить вычисление схемы
до момента фактической валидации значения.
import * as yup from 'yup';
const schemaA = yup.object({
b: yup.lazy(() => schemaB)
});
const schemaB = yup.object({
a: yup.lazy(() => schemaA)
});
Ключевая особенность заключается в том, что функция внутри
lazy вызывается только в момент необходимости проверки
значения, а не при создании схемы.
Это переносит проблему из области инициализации в область выполнения, где уже есть контекст данных, позволяющий корректно выбрать нужную ветку схемы.
Часто циклы возникают в моделях данных, где сущности взаимосвязаны:
В Yup такие структуры требуют осторожного моделирования.
Пример рекурсивного дерева:
import * as yup from 'yup';
const node = yup.object({
id: yup.string().required(),
children: yup.array().of(
yup.lazy(() => node)
)
});
Здесь цикл не является проблемой, потому что ссылка на
node обёрнута в lazy, и вычисление структуры
откладывается до анализа каждого элемента массива.
Массивы часто становятся естественной точкой возникновения рекурсии. Особенно это характерно для структур:
При прямом описании без lazy возникает немедленное
развертывание схемы:
const menu = yup.object({
title: yup.string(),
items: yup.array().of(menu)
});
Такая конструкция приводит к немедленной самоссылке и невозможности корректной инициализации.
Корректная форма:
const menu = yup.object({
title: yup.string(),
items: yup.array().of(
yup.lazy(() => menu)
)
});
Отдельный класс проблем связан не с Yup напрямую, а с системой модулей ES.
При использовании структуры файлов:
schemaA.js -> schemaB.js -> schemaA.js
происходит частичная загрузка модулей. В JavaScript это выражается в следующем:
undefinedПример:
// schemaA.js
import { schemaB } from './schemaB.js';
export const schemaA = yup.object({
b: yup.lazy(() => schemaB)
});
// schemaB.js
import { schemaA } from './schemaA.js';
export const schemaB = yup.object({
a: yup.lazy(() => schemaA)
});
Даже при использовании lazy проблема может сохраняться,
если сам модуль импортируется до завершения инициализации.
Наиболее устойчивый подход — устранение циклов на уровне архитектуры
схем, а не только через lazy.
Распространённые методы:
1. Выделение общих под-схем
Повторяющиеся части выносятся в отдельные независимые модули:
export const baseUser = yup.object({
id: yup.string().required()
});
2. Использование фабрик схем
Схема создаётся как функция, а не как статический объект:
export const createUserSchema = () =>
yup.object({
friend: yup.lazy(() => createUserSchema())
});
3. Локализация зависимостей
Схемы определяются в одном файле, чтобы исключить циклы импорта.
Несмотря на гибкость lazy, существуют ограничения:
При работе с рекурсивными структурами важно учитывать, что Yup выполняет реальную проверку данных, а не только их описание. Поэтому каждая рекурсивная ветка фактически приводит к новому вызову валидатора.
При наличии циклических структур в данных возможны следующие эффекты:
Yup не содержит встроенного механизма детектирования уже посещённых узлов, поэтому защита от бесконечной рекурсии полностью возлагается на структуру схемы и входных данных.
Наиболее устойчивые модели строятся по принципу:
lazyПример ограничения глубины:
const tree = yup.object({
value: yup.string(),
depth: yup.number(),
children: yup.lazy((value) => {
if (value && value.depth > 5) {
return yup.mixed().strip();
}
return yup.array().of(tree);
})
});
Когда структура данных представляет собой граф, а не дерево, Yup становится менее предсказуемым инструментом. В графах:
В таких случаях схема валидации должна быть адаптирована к модели графа, часто с переходом к идентификаторам вместо вложенных объектов:
const graphNode = yup.object({
id: yup.string(),
parentIds: yup.array(yup.string())
});
Такая модель полностью устраняет циклы на уровне структуры.
Схемы с циклическими зависимостями обладают повышенной сложностью сопровождения:
Поэтому даже при технической возможности использования
yup.lazy циклы считаются архитектурным запахом и требуют
пересмотра модели данных.