Вложенные объекты

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


Базовое описание вложенного объекта

В Joi объект описывается через Joi.object(), а его структура задаётся методом keys():

const Joi = require('joi');

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().min(2).max(50).required(),
    age: Joi.number().integer().min(0).max(120)
  })
});

В данном примере поле user является вложенным объектом, содержащим два поля с собственными правилами валидации.


Обязательные и необязательные вложенные поля

Вложенные объекты подчиняются тем же правилам, что и обычные поля. Обязательность задаётся через required():

const schema = Joi.object({
  profile: Joi.object({
    username: Joi.string().alphanum().required(),
    bio: Joi.string().max(200).optional()
  }).required()
});

Если объект profile отсутствует, валидация завершится ошибкой. При этом поле bio может отсутствовать без нарушения правил.


Многоуровневая вложенность

Joi поддерживает произвольную глубину структур, включая вложенные объекты внутри вложенных объектов:

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().required(),
    contact: Joi.object({
      email: Joi.string().email().required(),
      phone: Joi.object({
        countryCode: Joi.string().required(),
        number: Joi.string().pattern(/^[0-9]+$/).required()
      })
    })
  })
});

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


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

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

const phoneSchema = Joi.object({
  countryCode: Joi.string().required(),
  number: Joi.string().required()
});

const contactSchema = Joi.object({
  email: Joi.string().email().required(),
  phone: phoneSchema
});

const userSchema = Joi.object({
  name: Joi.string().required(),
  contact: contactSchema
});

Такой подход уменьшает дублирование и упрощает поддержку.


Валидация массивов вложенных объектов

Часто вложенные объекты комбинируются с массивами:

const schema = Joi.object({
  users: Joi.array().items(
    Joi.object({
      id: Joi.number().required(),
      name: Joi.string().required(),
      address: Joi.object({
        city: Joi.string().required(),
        zip: Joi.string().required()
      })
    })
  )
});

Каждый элемент массива проходит валидацию по одинаковой вложенной схеме.


Частичная валидация вложенных структур

В некоторых случаях требуется разрешить дополнительные поля внутри вложенных объектов:

const schema = Joi.object({
  settings: Joi.object({
    theme: Joi.string().valid('dark', 'light'),
    notifications: Joi.object({
      email: Joi.boolean(),
      sms: Joi.boolean()
    }).unknown(true)
  })
});

Метод unknown(true) разрешает наличие дополнительных ключей, не описанных в схеме.


Разделение схем через keys()

Метод keys() используется для явного определения структуры объекта:

const addressSchema = Joi.object().keys({
  street: Joi.string().required(),
  city: Joi.string().required(),
  country: Joi.string().required()
});

Вложение этой схемы в другой объект остаётся прозрачным:

const schema = Joi.object({
  user: Joi.object({
    name: Joi.string().required(),
    address: addressSchema
  })
});

Условная логика во вложенных объектах

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

const schema = Joi.object({
  payment: Joi.object({
    method: Joi.string().valid('card', 'cash').required(),
    cardDetails: Joi.object({
      number: Joi.string().creditCard().required(),
      cvv: Joi.string().required()
    }).when('method', {
      is: 'card',
      then: Joi.required(),
      otherwise: Joi.forbidden()
    })
  })
});

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


Клонирование и расширение вложенных схем

Схемы можно расширять без изменения оригинала:

const baseUser = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email().required()
});

const extendedUser = baseUser.keys({
  role: Joi.string().valid('admin', 'user')
});

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


Валидация глубоко вложенных структур с альтернативами

Для сложных моделей данных иногда требуется альтернативная структура вложенного объекта:

const schema = Joi.object({
  config: Joi.alternatives().try(
    Joi.object({
      mode: Joi.string().valid('simple')
    }),
    Joi.object({
      mode: Joi.string().valid('advanced'),
      options: Joi.object({
        retries: Joi.number(),
        timeout: Joi.number()
      })
    })
  )
});

Каждая альтернатива может содержать собственную вложенную структуру.


Типовые ошибки при работе с вложенными объектами

Часто встречаются ошибки, связанные с неполным описанием структуры:

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

В этом случае отсутствует Joi.object(), из-за чего вложенность не распознаётся корректно. Правильный вариант:

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

Наследование структур и композиция

При проектировании схем сложных данных удобно использовать композицию:

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

const articleSchema = Joi.object({
  title: Joi.string().required(),
  content: Joi.string().required(),
  metadata: metadataSchema
});

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