Ленивая валидация

Ленивая валидация в Joi основана на идее отложенного построения схемы: структура правила не фиксируется заранее, а вычисляется в момент проверки данных. Это позволяет описывать динамические и рекурсивные структуры, которые невозможно выразить через статические схемы.

В обычном подходе схема Joi создаётся как набор заранее известных правил. Например:

const schema = Joi.object({
  type: Joi.string().valid('A', 'B'),
  value: Joi.string().required()
});

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


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

Ключевая идея:

  • сначала анализируется входное значение
  • затем динамически выбирается схема валидации
  • после этого выполняется проверка

Это особенно полезно для:

  • рекурсивных структур
  • полиморфных объектов
  • зависимых типов данных
  • условной валидации, зависящей от значения поля

Joi.lazy()

Основной механизм ленивой валидации реализован через 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: []
    }
  ]
};

Такая схема широко используется для:

  • DOM-подобных структур
  • меню интерфейса
  • файловых систем
  • графов

Динамический выбор схемы по содержимому

Ленивая валидация позволяет строить схемы на основе значения конкретного поля.

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.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:

  • 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"]

Типичные ошибки при использовании Joi.lazy()

Бесконечная рекурсия

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

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-схему.


Производственные сценарии применения

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

  • API с версионированием форматов
  • динамические формы
  • конфигурационные движки
  • интерпретаторы данных
  • вложенные JSON-структуры с разными типами узлов

Композиция ленивых схем

Схемы можно комбинировать, создавая цепочки динамической логики:

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 или невалидный тип схемы, результатом будет системная ошибка валидации.


Ограничения ленивой валидации

Несмотря на гибкость, подход имеет ограничения:

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

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