Ancestors и siblings

Валидация в Joi строится вокруг возможности описывать зависимости между полями. Эти зависимости делятся на два основных класса: связи между полями одного уровня (siblings) и обращения к значениям вышестоящих объектов (ancestors). Именно они позволяют реализовывать межполевую логику: сравнение значений, условные ограничения и каскадную валидацию вложенных структур.


Siblings: ссылки между полями одного объекта

Siblings — это поля, находящиеся на одном уровне объекта. В Joi доступ к ним реализуется через Joi.ref, позволяющий ссылаться на другое свойство внутри того же объекта.

Базовый механизм Joi.ref

const schema = Joi.object({
  password: Joi.string().min(8).required(),
  confirmPassword: Joi.string().valid(Joi.ref('password')).required()
});

В этом примере confirmPassword сравнивается со значением password. Оба поля являются siblings, так как принадлежат одному объекту.

Ключевой момент: Joi.ref('password') интерпретируется как путь внутри текущего объекта.


Сравнение значений siblings

Частый сценарий — проверка диапазонов или логических ограничений между полями:

const schema = Joi.object({
  startDate: Joi.date().required(),
  endDate: Joi.date().min(Joi.ref('startDate')).required()
});

Здесь endDate зависит от startDate, и Joi автоматически обеспечивает корректность порядка дат.


Условная логика между siblings

Метод when позволяет строить ветвления, основанные на значениях других полей:

const schema = Joi.object({
  role: Joi.string().valid('admin', 'user').required(),

  accessLevel: Joi.when('role', {
    is: 'admin',
    then: Joi.number().min(10),
    otherwise: Joi.number().min(1).max(5)
  })
});

Поле accessLevel изменяет правила валидации в зависимости от sibling role.


Расширенные ссылки через ref options

Joi.ref поддерживает дополнительные параметры, влияющие на интерпретацию пути:

Joi.ref('password', { separator: '.' })

Это позволяет обращаться к вложенным структурам внутри siblings:

const schema = Joi.object({
  user: Joi.object({
    password: Joi.string().required()
  }),
  confirm: Joi.string().valid(Joi.ref('user.password'))
});

Ancestors: доступ к вышестоящим объектам

Ancestors — это родительские уровни вложенности. В сложных структурах данных поле может зависеть не только от siblings, но и от значений выше по дереву.

Вложенные объекты и контекст пути

const schema = Joi.object({
  account: Joi.object({
    type: Joi.string().valid('premium', 'basic').required(),
    limits: Joi.object({
      requests: Joi.number().when(Joi.ref('/account/type'), {
        is: 'premium',
        then: Joi.number().max(10000),
        otherwise: Joi.number().max(1000)
      })
    })
  })
});

Здесь используется абсолютная ссылка /account/type, которая выходит за пределы текущего объекта limits и обращается к ancestor.


Абсолютные и относительные пути

Joi поддерживает два подхода:

Абсолютный путь

Начинается с / и игнорирует текущий уровень вложенности:

Joi.ref('/user/settings/theme')

Относительный путь

Интерпретируется относительно текущего объекта:

Joi.ref('settings.theme')

Управление уровнями ancestors

В сложных структурах важно управлять тем, на каком уровне выполняется разрешение ссылки.

Joi.ref('type', { ancestor: 1 })

Параметр ancestor указывает, насколько уровней вверх подниматься при поиске значения:

  • 0 — текущий объект
  • 1 — родитель
  • 2 — прародитель

Взаимодействие siblings и ancestors в одной схеме

Комбинирование обоих механизмов позволяет строить многоуровневую бизнес-логику.

const schema = Joi.object({
  organization: Joi.object({
    plan: Joi.string().valid('free', 'pro').required(),

    user: Joi.object({
      role: Joi.string().required(),

      quota: Joi.number().when(Joi.ref('/organization/plan'), {
        is: 'free',
        then: Joi.number().max(100),
        otherwise: Joi.number().max(10000)
      })
    })
  })
});

Здесь:

  • plan и user находятся как siblings внутри organization
  • quota зависит от ancestor /organization/plan

Поведение при отсутствующих значениях

Ссылки в Joi чувствительны к наличию данных:

  • если referenced field отсутствует, результат ref становится undefined
  • valid(Joi.ref(...)) может не пройти валидацию без явного required
  • порядок объявления схемы не влияет на разрешение ссылок, так как Joi обрабатывает дерево целиком

Вложенные зависимости и каскадные ограничения

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

const schema = Joi.object({
  a: Joi.number().required(),
  b: Joi.number().min(Joi.ref('a')),
  c: Joi.number().min(Joi.ref('b')),
  d: Joi.number().min(Joi.ref('c'))
});

Здесь формируется каскад siblings-зависимостей, где каждое поле опирается на предыдущее.


Типичные ошибки при работе с ancestors и siblings

Некорректный путь ссылки

Joi.ref('user.password') // ошибка при отсутствии структуры user

Неправильный уровень ancestor

Joi.ref('type', { ancestor: 5 }) // выходит за пределы дерева

Смешивание относительных и абсолютных путей без понимания контекста

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


Альтернативные механизмы

В случаях, когда Joi.ref и when недостаточны, применяется кастомная логика:

Joi.object({
  a: Joi.number(),
  b: Joi.number()
}).custom((value, helpers) => {
  if (value.b < value.a) {
    return helpers.error('any.invalid');
  }
  return value;
});

Этот подход полностью снимает ограничения на ancestors/siblings, но требует ручного управления логикой.