В библиотеке 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 особенно полезен, когда структура данных
заранее неизвестна или может быть рекурсивной.
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. Это позволяет строить гибридные схемы, где часть
логики зависит от значения, а часть — от типа данных.
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(),
});
})
);
Каждый элемент массива валидируется по собственной схеме, определяемой во время выполнения.
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(),
})
);
Это используется в структурах категорий, комментариев и вложенных сущностей.
При динамическом определении схем важно явно учитывать отсутствие
значения, так как 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,
подобный подход часто комбинируется с динамическими схемами для
адаптации под окружение.