Кросс-полевая валидация

Кросс-полевая валидация в Joi применяется в случаях, когда корректность одного поля зависит от значения другого. В отличие от простой валидации отдельных значений, здесь проверяется согласованность структуры объекта в целом: соответствие диапазонов, совпадение парных значений, взаимная обязательность или исключение полей.

Основой механизма выступают ссылки на другие поля, условные схемы и набор зависимостей между ключами.


Механизм ссылок Joi.ref

Ссылки позволяют обращаться к значению другого поля внутри схемы. Это базовый инструмент кросс-полевой логики.

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

В данном случае значение confirmPassword должно строго совпадать с password.

Ссылки могут использоваться не только в valid, но и в преобразованиях:

Joi.number().min(Joi.ref('minValue'))

или в условиях:

Joi.string().when('role', {
  is: 'admin',
  then: Joi.required(),
  otherwise: Joi.optional()
});

Условная валидация через when

Метод when реализует динамическое поведение схемы в зависимости от других полей.

Базовая форма

const schema = Joi.object({
  type: Joi.string().valid('individual', 'company').required(),
  companyName: Joi.string().when('type', {
    is: 'company',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

Сравнение с диапазонами значений

const schema = Joi.object({
  minAge: Joi.number().required(),
  maxAge: Joi.number().when('minAge', {
    is: Joi.number().required(),
    then: Joi.number().min(Joi.ref('minAge'))
  })
});

Такой подход обеспечивает согласованность диапазонов внутри одного объекта.


Взаимные зависимости ключей

Joi предоставляет набор декларативных правил для описания связей между полями.

with

Требует присутствия одного поля при наличии другого.

Joi.object({
  cardNumber: Joi.string(),
  cvc: Joi.string()
}).with('cardNumber', 'cvc');

without

Запрещает совместное использование полей.

Joi.object({
  token: Joi.string(),
  password: Joi.string()
}).without('token', 'password');

xor

Разрешает только одно из полей.

Joi.object({
  email: Joi.string().email(),
  phone: Joi.string()
}).xor('email', 'phone');

or

Требует хотя бы одно поле из набора.

Joi.object({
  email: Joi.string().email(),
  phone: Joi.string()
}).or('email', 'phone');

and

Требует совместного присутствия всех указанных полей.

Joi.object({
  a: Joi.string(),
  b: Joi.string(),
  c: Joi.string()
}).and('a', 'b', 'c');

nand

Запрещает одновременное наличие всех полей.

Joi.object({
  start: Joi.date(),
  end: Joi.date()
}).nand('start', 'end');

Сравнение значений полей

Кросс-полевая логика часто используется для проверки равенства или порядка значений.

Равенство строк

Joi.object({
  password: Joi.string().required(),
  repeat: Joi.string().valid(Joi.ref('password'))
});

Сравнение чисел

Joi.object({
  from: Joi.number().required(),
  to: Joi.number().greater(Joi.ref('from'))
});

Проверка дат

Joi.object({
  startDate: Joi.date().required(),
  endDate: Joi.date().greater(Joi.ref('startDate'))
});

Такие конструкции формируют строгую согласованность временных и числовых диапазонов.


Валидация диапазонов и связанных ограничений

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

Joi.object({
  minPrice: Joi.number().required(),
  maxPrice: Joi.number()
    .required()
    .min(Joi.ref('minPrice'))
    .less(10000)
});

Комбинация min, max, greater, less и ref позволяет задавать сложные ограничения без кастомного кода.


Использование context для динамических зависимостей

Контекстные значения позволяют внедрять внешние параметры в схему.

const schema = Joi.object({
  age: Joi.number().min(Joi.ref('$minAge'))
});

При валидации:

schema.validate(data, { context: { minAge: 18 } });

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


alternatives() для сложных кросс-структур

Когда логика выходит за рамки простых зависимостей, применяется alternatives.

const schema = Joi.object({
  payment: Joi.alternatives().try(
    Joi.object({
      type: Joi.valid('card'),
      cardNumber: Joi.string().required()
    }),
    Joi.object({
      type: Joi.valid('cash'),
      cashOnly: Joi.boolean().valid(true)
    })
  )
});

alternatives позволяет описывать взаимоисключающие структуры внутри одного поля или объекта.


Паттерны кросс-полевой валидации

Подтверждение пароля

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

Условная обязательность

Joi.object({
  hasDiscount: Joi.boolean(),
  discountCode: Joi.string().when('hasDiscount', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.forbidden()
  })
});

Зависимость диапазонов

Joi.object({
  start: Joi.number().required(),
  end: Joi.number().greater(Joi.ref('start'))
});

Взаимоисключающие поля

Joi.object({
  username: Joi.string(),
  email: Joi.string()
}).xor('username', 'email');

Частые ошибки при кросс-полевой валидации

Использование ссылок без учёта преобразований приводит к несоответствиям, особенно при convert: true, когда типы приводятся автоматически.

Некорректное применение when с неполными условиями создаёт ситуации, при которых часть веток схемы остаётся без явной валидации.

Смешивание required() и forbidden() внутри разных веток условной логики может приводить к неоднозначным схемам, особенно при глубокой вложенности объектов.


Логическая композиция зависимостей

В сложных моделях данных кросс-полевая валидация строится как комбинация:

  • ссылок (ref)
  • условий (when)
  • зависимостей (with, xor, or)
  • альтернатив (alternatives)
  • контекстных параметров (context)

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