Concat для композиции схем

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

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


Метод concat

Метод concat используется для объединения двух схем одного типа в новую схему, содержащую правила обеих исходных.

Сигнатура:

schema.concat(otherSchema)

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

Пример объединения объектных схем:

const Joi = require('joi');

const baseSchema = Joi.object({
  id: Joi.number().integer().required(),
  createdAt: Joi.date().required()
});

const extendedSchema = Joi.object({
  name: Joi.string().min(3).max(30).required(),
  email: Joi.string().email().required()
});

const finalSchema = baseSchema.concat(extendedSchema);

В результате finalSchema будет проверять объект, содержащий все поля из обеих схем.


Правила слияния схем

При использовании concat Joi выполняет объединение на уровне описания правил каждого поля.

Для объектов:

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

Пример пересечения правил:

const schemaA = Joi.object({
  age: Joi.number().min(0)
});

const schemaB = Joi.object({
  age: Joi.number().max(100)
});

const schemaC = schemaA.concat(schemaB);

Результирующее поле age будет иметь одновременно ограничения:

  • минимум 0
  • максимум 100

Поведение при конфликте правил

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

const schemaA = Joi.object({
  status: Joi.string().valid('active')
});

const schemaB = Joi.object({
  status: Joi.string().valid('inactive')
});

const schemaC = schemaA.concat(schemaB);

В данном случае значение status должно одновременно удовлетворять двум несовместимым множествам допустимых значений. Такая схема становится невыполнимой.


Композиция на уровне строк, чисел и массивов

Метод concat применим не только к объектам, но и к другим типам схем.

Строки

const base = Joi.string().min(5);
const extended = Joi.string().max(10);

const result = base.concat(extended);

Итоговая схема задаёт диапазон длины строки от 5 до 10 символов.


Числа

const base = Joi.number().integer();
const extended = Joi.number().positive();

const result = base.concat(extended);

Результат объединяет оба ограничения: число должно быть целым и положительным.


Массивы

const base = Joi.array().items(Joi.number());
const extended = Joi.array().min(2);

const result = base.concat(extended);

Полученная схема требует массив чисел длиной не менее двух элементов.


Композиция сложных объектов

На практике concat часто используется для построения схем по принципу слоёв: базовая структура + расширения под конкретные сценарии.

const baseUser = Joi.object({
  id: Joi.number().required(),
  role: Joi.string().required()
});

const contactInfo = Joi.object({
  email: Joi.string().email(),
  phone: Joi.string()
});

const profile = Joi.object({
  nickname: Joi.string()
});

const fullUserSchema = baseUser
  .concat(contactInfo)
  .concat(profile);

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


Поведение вложенных объектов

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

const schemaA = Joi.object({
  user: Joi.object({
    id: Joi.number().required()
  })
});

const schemaB = Joi.object({
  user: Joi.object({
    name: Joi.string().required()
  })
});

const result = schemaA.concat(schemaB);

В результате поле user будет содержать оба правила, но итоговое поведение зависит от того, как Joi объединит внутренние схемы объектов.


Приоритеты и переопределения

concat не поддерживает прямое переопределение правил в классическом смысле. Вместо этого:

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

Это означает, что попытка “заменить” правило через concat не приведёт к удалению старого ограничения.


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

При работе с alternatives композиция схем требует особого внимания:

const base = Joi.alternatives().try(
  Joi.string(),
  Joi.number()
);

const extended = Joi.alternatives().try(
  Joi.string().email()
);

const result = base.concat(extended);

В таких случаях итоговая схема расширяет набор допустимых альтернатив, не исключая исходные варианты.


Модульная архитектура схем

Метод concat часто применяется для построения схем в модульной архитектуре:

  • базовые схемы содержат общие поля
  • расширения добавляют контекстные ограничения
  • финальная схема собирается в точке использования

Пример структуры:

const idSchema = Joi.object({
  id: Joi.number().required()
});

const timestampSchema = Joi.object({
  createdAt: Joi.date(),
  updatedAt: Joi.date()
});

const entitySchema = idSchema.concat(timestampSchema);

Типовые ошибки при использовании concat

  1. Объединение несовместимых типов
Joi.string().concat(Joi.number())

Такая операция невозможна из-за различия типов схем.

  1. Конфликт валидных значений Пересечение valid() с несовместимыми значениями приводит к невыполнимым схемам.

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

  3. Попытка глубокого рекурсивного слияния Вложенные объекты не всегда объединяются так, как предполагается логикой приложения.


Стратегии построения схем через композицию

Использование concat эффективно в сочетании с разбиением схем на логические блоки:

  • идентификация сущности (id, uuid)
  • метаданные (timestamps)
  • бизнес-поля (name, status)
  • контекстные расширения (permissions, flags)

Такой подход формирует предсказуемую и расширяемую структуру валидации без дублирования описаний.