Ленивая валидация в Joi основана на идее отложенного построения схемы: структура правила не фиксируется заранее, а вычисляется в момент проверки данных. Это позволяет описывать динамические и рекурсивные структуры, которые невозможно выразить через статические схемы.
В обычном подходе схема Joi создаётся как набор заранее известных правил. Например:
const schema = Joi.object({
type: Joi.string().valid('A', 'B'),
value: Joi.string().required()
});
Такой подход хорошо работает, когда структура данных стабильна. Однако в реальных системах часто встречаются ситуации, когда тип данных определяет его дальнейшую форму. Именно здесь применяется ленивое построение схем.
Ленивая валидация означает, что схема создаётся не сразу, а в момент анализа конкретного значения. Это позволяет учитывать содержимое входных данных при формировании правил проверки.
Ключевая идея:
Это особенно полезно для:
Основной механизм ленивой валидации реализован через
Joi.lazy().
Сигнатура:
Joi.lazy((value, helpers) => schema)
Функция принимает текущее значение и возвращает схему Joi, которая будет использована для проверки.
Пример базового использования:
const Joi = require('joi');
const schema = Joi.lazy((value) => {
if (typeof value === 'string') {
return Joi.string().min(3);
}
if (typeof value === 'number') {
return Joi.number().integer();
}
return Joi.any().forbidden();
});
Здесь тип данных определяет, какая схема будет применена.
Одно из ключевых применений ленивой валидации — рекурсивные данные, например дерево.
const nodeSchema = Joi.object({
value: Joi.string().required(),
children: Joi.array().items(Joi.lazy(() => nodeSchema))
});
Без Joi.lazy() невозможно было бы сослаться на схему,
которая ещё не определена. Ленивая функция разрывает цикл определения и
позволяет описывать самоссылочные структуры.
Пример данных:
const data = {
value: 'root',
children: [
{
value: 'child1',
children: []
}
]
};
Такая схема широко используется для:
Ленивая валидация позволяет строить схемы на основе значения конкретного поля.
const schema = Joi.object({
type: Joi.string().valid('user', 'admin').required(),
data: Joi.lazy((value, helpers) => {
const parent = helpers.state.ancestors[0];
if (parent.type === 'user') {
return Joi.object({
name: Joi.string().required(),
age: Joi.number().min(0)
});
}
if (parent.type === 'admin') {
return Joi.object({
permissions: Joi.array().items(Joi.string())
});
}
return Joi.any().forbidden();
})
});
Здесь структура data зависит от поля type в
том же объекте.
Часто ленивую валидацию сравнивают с Joi.when(), но
между ними есть принципиальная разница:
Joi.when() работает декларативноJoi.lazy() работает вычислительноJoi.when():
const schema = Joi.object({
type: Joi.string(),
value: Joi.when('type', {
is: 'number',
then: Joi.number(),
otherwise: Joi.string()
})
});
Joi.lazy():
const schema = Joi.object({
type: Joi.string(),
value: Joi.lazy((value, helpers) => {
const parent = helpers.state.ancestors[0];
return parent.type === 'number'
? Joi.number()
: Joi.string();
})
});
Разница:
when описывает условия заранееlazy вычисляет схему в рантаймеВ ленивой функции доступны вспомогательные данные через
helpers:
helpers.state.ancestors — цепочка родительских
объектовhelpers.path — путь до текущего значенияhelpers.prefs — настройки валидацииПример использования предков:
Joi.lazy((value, helpers) => {
const root = helpers.state.ancestors[0];
if (root.version === 1) {
return Joi.string();
}
return Joi.number();
});
Это позволяет строить сложные зависимости между уровнями вложенности.
Ленивая валидация особенно полезна для массивов, содержащих разные типы элементов.
const schema = Joi.array().items(
Joi.lazy((value) => {
if (typeof value === 'number') {
return Joi.number().integer();
}
if (typeof value === 'string') {
return Joi.string().min(1);
}
return Joi.any().forbidden();
})
);
Пример допустимых данных:
[1, 2, "text", 5, "abc"]
Неправильное построение схемы может привести к циклическим вызовам без базового условия:
const schema = Joi.lazy(() => schema);
Такой код приводит к переполнению стека.
Правильный подход требует условия выхода:
const schema = Joi.lazy(() => Joi.object({
value: Joi.string(),
next: Joi.alternatives().try(
Joi.link('#node'),
Joi.valid(null)
)
})).id('node');
Иногда схема возвращает некорректный тип Joi-объекта:
Joi.lazy(() => "string") // ошибка
Функция всегда должна возвращать валидную Joi-схему.
Ленивая валидация используется в системах, где структура данных заранее неизвестна:
Схемы можно комбинировать, создавая цепочки динамической логики:
const base = Joi.lazy((value) => {
if (Array.isArray(value)) {
return Joi.array().items(base);
}
if (typeof value === 'object') {
return Joi.object({
nested: base.optional()
});
}
return Joi.any();
});
Такая структура позволяет описывать произвольную глубину вложенности.
Ошибки внутри ленивой схемы обрабатываются так же, как и в обычных схемах Joi. Однако важно учитывать, что ошибка может возникнуть на этапе выбора схемы, а не только проверки значения.
Например, если функция возвращает undefined или
невалидный тип схемы, результатом будет системная ошибка валидации.
Несмотря на гибкость, подход имеет ограничения:
Поэтому ленивую валидацию целесообразно применять только там, где структура действительно зависит от данных, а не как универсальный механизм построения схем.