Зависимости между полями

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

Зависимости позволяют:

  • требовать наличие одного поля при существовании другого;
  • запрещать одновременное использование нескольких свойств;
  • задавать альтернативные сценарии заполнения объекта;
  • проверять условия между значениями;
  • реализовывать сложные бизнес-правила.

Методы зависимостей объектов

Основные механизмы взаимосвязей работают на уровне Joi.object().

with()

Метод with() требует наличие одного поля вместе с другим.

Синтаксис

object.with(source, required)
  • source — поле-инициатор;
  • required — обязательное поле.

Пример

const Joi = require('joi');

const schema = Joi.object({
    login: Joi.string(),
    password: Joi.string()
}).with('login', 'password');

Поведение

Если присутствует login, обязательно должен существовать password.

Корректные данные:

{
    login: 'admin',
    password: '123456'
}
{}

Ошибка:

{
    login: 'admin'
}

Несколько зависимостей

const schema = Joi.object({
    username: Joi.string(),
    password: Joi.string(),
    email: Joi.string()
}).with('username', ['password', 'email']);

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


without()

Метод without() запрещает наличие одного поля вместе с другим.

Пример

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

Логика

Если существует token, поле password запрещено.

Допустимо:

{
    token: 'abc123'
}
{
    password: '123456'
}

Ошибка:

{
    token: 'abc123',
    password: '123456'
}

and()

Метод and() требует совместного существования группы полей.

Пример

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

Поведение

  • если указано startDate, нужен endDate;
  • если указан endDate, нужен startDate.

Ошибка:

{
    startDate: '2025-01-01'
}

or()

Метод or() требует наличие хотя бы одного поля из списка.

Пример

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

Валидные варианты

{
    email: 'user@mail.com'
}
{
    phone: '+77001234567'
}
{
    email: 'user@mail.com',
    phone: '+77001234567'
}

Ошибка

{}

xor()

Метод xor() требует наличие только одного поля из группы.

Пример

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

Допустимо

{
    email: 'user@mail.com'
}

или

{
    phone: '+77001234567'
}

Ошибка

Оба поля одновременно:

{
    email: 'user@mail.com',
    phone: '+77001234567'
}

Отсутствие обоих полей:

{}

oxor()

Метод oxor() похож на xor(), но поля становятся необязательными.

Пример

const schema = Joi.object({
    email: Joi.string(),
    phone: Joi.string()
}).oxor('email', 'phone');

Допустимо

{}
{
    email: 'user@mail.com'
}

Ошибка

{
    email: 'user@mail.com',
    phone: '+77001234567'
}

nand()

Метод nand() запрещает совместное наличие полей.

Пример

const schema = Joi.object({
    isAdmin: Joi.boolean(),
    guest: Joi.boolean()
}).nand('isAdmin', 'guest');

Ошибка

{
    isAdmin: true,
    guest: true
}

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

Метод when() — основной инструмент для создания динамических схем.


Базовый синтаксис

Joi.when(reference, options)

Параметры:

  • reference — поле для проверки;
  • options — условия.

Простая условная проверка

Обязательное поле при выполнении условия

const schema = Joi.object({
    role: Joi.string(),

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

Поведение

Если:

{
    role: 'admin'
}

то возникнет ошибка — отсутствует permissions.

Для обычного пользователя:

{
    role: 'user'
}

поле permissions запрещено.


Изменение типа поля

when() способен менять не только обязательность, но и всю схему.

Пример

const schema = Joi.object({
    type: Joi.string(),

    value: Joi.when('type', {
        is: 'number',
        then: Joi.number(),
        otherwise: Joi.string()
    })
});

Валидные варианты

{
    type: 'number',
    value: 100
}
{
    type: 'text',
    value: 'hello'
}

Использование нескольких условий

switch()

Для сложной логики применяется switch.

Пример

const schema = Joi.object({
    role: Joi.string(),

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

Проверка нескольких полей

when() может анализировать объект целиком.

Пример

const schema = Joi.object({
    country: Joi.string(),
    city: Joi.string(),
    zip: Joi.string()
}).when(
    Joi.object({
        country: Joi.valid('USA')
    }).unknown(),
    {
        then: Joi.object({
            zip: Joi.required()
        })
    }
);

Если страна — USA, индекс становится обязательным.


Ссылки между полями

Joi.ref()

Joi.ref() позволяет ссылаться на значение другого поля.


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

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

const schema = Joi.object({
    password: Joi.string().required(),

    confirmPassword: Joi.string()
        .valid(Joi.ref('password'))
        .required()
});

Ошибка

{
    password: '123456',
    confirmPassword: 'qwerty'
}

Числовые зависимости

const schema = Joi.object({
    min: Joi.number(),

    max: Joi.number().greater(Joi.ref('min'))
});

Проверка

max должен быть больше min.


Проверка дат

const schema = Joi.object({
    startDate: Joi.date(),

    endDate: Joi.date().greater(Joi.ref('startDate'))
});

Использование относительных ссылок

Вложенные объекты поддерживают относительные ссылки.

Пример

const schema = Joi.object({
    credentials: Joi.object({
        password: Joi.string(),

        confirmPassword: Joi.string()
            .valid(Joi.ref('password'))
    })
});

Абсолютные ссылки

Для обращения к корню схемы используется /.

Пример

const schema = Joi.object({
    role: Joi.string(),

    profile: Joi.object({
        access: Joi.string().when(Joi.ref('/role'), {
            is: 'admin',
            then: Joi.required()
        })
    })
});

Массивы и зависимости

Проверка элементов массива

const schema = Joi.object({
    tags: Joi.array().items(
        Joi.object({
            type: Joi.string(),

            value: Joi.when('type', {
                is: 'system',
                then: Joi.string().required(),
                otherwise: Joi.number().required()
            })
        })
    )
});

Условная структура объектов

Полное изменение схемы

const schema = Joi.object({
    paymentType: Joi.string().required()
}).when(
    Joi.object({
        paymentType: Joi.valid('card')
    }).unknown(),
    {
        then: Joi.object({
            cardNumber: Joi.string().required(),
            cvv: Joi.string().required()
        }),

        otherwise: Joi.object({
            cashbox: Joi.string().required()
        })
    }
);

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

Joi.alternatives()

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

Пример

const schema = Joi.alternatives().try(
    Joi.object({
        type: Joi.valid('email'),
        email: Joi.string().email().required()
    }),

    Joi.object({
        type: Joi.valid('phone'),
        phone: Joi.string().required()
    })
);

Зависимости и кастомная логика

custom()

При невозможности выразить правило стандартными средствами используется custom().

Пример

const schema = Joi.object({
    start: Joi.number(),
    end: Joi.number()
}).custom((value, helpers) => {

    if (value.end <= value.start) {
        return helpers.error('any.invalid');
    }

    return value;
});

Контекстная валидация

Joi поддерживает передачу внешнего контекста.

Пример

const schema = Joi.object({
    role: Joi.string()
}).when('$isAdmin', {
    is: true,
    then: Joi.object({
        role: Joi.valid('admin')
    })
});

Вызов:

schema.validate(data, {
    context: {
        isAdmin: true
    }
});

Комбинирование зависимостей

Механизмы можно объединять.

Пример

const schema = Joi.object({
    username: Joi.string(),

    password: Joi.string(),

    token: Joi.string(),

    confirmPassword: Joi.string()
        .valid(Joi.ref('password'))
}).with('username', 'password')
  .without('token', 'password');

Правила

  • username требует password;
  • token запрещает password;
  • confirmPassword должен совпадать с password.

Управление сообщениями ошибок

Локальные сообщения

const schema = Joi.object({
    password: Joi.string(),

    confirmPassword: Joi.string()
        .valid(Joi.ref('password'))
        .messages({
            'any.only': 'Пароли не совпадают'
        })
});

Сообщения для зависимостей

const schema = Joi.object({
    email: Joi.string(),
    phone: Joi.string()
}).xor('email', 'phone')
  .messages({
      'object.xor': 'Укажите либо email, либо phone'
  });

Практические сценарии

Авторизация

const schema = Joi.object({
    login: Joi.string(),

    password: Joi.string(),

    token: Joi.string()
}).xor('password', 'token')
  .with('login', 'password');

Регистрация пользователя

const schema = Joi.object({
    email: Joi.string().email().required(),

    password: Joi.string().min(8).required(),

    confirmPassword: Joi.string()
        .valid(Joi.ref('password'))
        .required(),

    age: Joi.number(),

    parentConsent: Joi.boolean()
}).when(
    Joi.object({
        age: Joi.number().less(18)
    }).unknown(),
    {
        then: Joi.object({
            parentConsent: Joi.valid(true).required()
        })
    }
);

Фильтрация данных

const schema = Joi.object({
    from: Joi.date(),
    to: Joi.date(),

    limit: Joi.number(),

    offset: Joi.number()
})
.and('fr om', 'to')
.with('offset', 'lim it');

Типичные ошибки

Неправильный путь в ref()

Ошибка:

Joi.ref('user.password')

при отсутствии вложенного объекта.


Конфликт зависимостей

.with('a', 'b')
.without('a', 'b')

Такая схема содержит логическое противоречие.


Использование when() вместо object-зависимостей

Многие задачи проще описываются через:

.and()
.or()
.xor()

чем через сложные конструкции when().


Рекомендации по проектированию схем

Разделение бизнес-логики

Схема Joi должна проверять:

  • структуру данных;
  • зависимости;
  • базовые ограничения.

Сложные вычисления и бизнес-процессы лучше выносить отдельно.


Использование небольших схем

Крупные условные конструкции удобно разбивать:

const credentialsSchema = Joi.object({
    login: Joi.string(),
    password: Joi.string()
});

const profileSchema = Joi.object({
    firstName: Joi.string(),
    lastName: Joi.string()
});

Повторное использование зависимостей

const passwordConfirmation = Joi.string()
    .valid(Joi.ref('password'))
    .required();

Производительность

Большое количество вложенных when() может усложнять выполнение валидации.

Особенно дорого обходятся:

  • глубокие вложенные условия;
  • массивы с динамическими схемами;
  • многочисленные custom() проверки.

Для оптимизации:

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

Сравнение подходов

Подход Назначение
with() одно поле требует другое
without() запрет совместного использования
and() поля должны существовать вместе
or() минимум одно поле
xor() только одно поле
when() условная схема
ref() ссылки между значениями
alternatives() альтернативные структуры
custom() произвольная логика