Вложенные схемы

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

В основе большинства сложных схем лежит object(). Этот тип схемы задаёт структуру объекта и определяет правила валидации для каждого его поля отдельно.

import * as Yup from 'yup';

const schema = Yup.object({
  name: Yup.string().required(),
  age: Yup.number().min(18),
  address: Yup.object({
    city: Yup.string().required(),
    zip: Yup.string().length(6)
  })
});

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

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

Вложенные объекты и путь валидации

При работе с вложенными схемами Yup формирует путь к каждому значению. Этот путь используется для определения источника ошибки.

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

const schema = Yup.object({
  user: Yup.object({
    profile: Yup.object({
      email: Yup.string().email().required()
    })
  })
});

Если значение email невалидно, ошибка будет ассоциирована с путём:

user.profile.email

Это поведение критически важно при интеграции с формами, где требуется точечное отображение ошибок.

Массивы как контейнер вложенных схем

Одним из наиболее распространённых случаев вложенности является использование массивов объектов. Для этого применяется array().of().

const schema = Yup.object({
  users: Yup.array().of(
    Yup.object({
      id: Yup.number().required(),
      name: Yup.string().required()
    })
  )
});

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

Дополнительно возможно ограничение длины массива:

Yup.array()
  .of(Yup.string())
  .min(1)
  .max(10)

Глубокая вложенность и композиция схем

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

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

const userSchema = Yup.object({
  name: Yup.string().required(),
  address: addressSchema
});

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

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

Сложные структуры часто включают комбинацию массивов и объектов на нескольких уровнях.

const schema = Yup.object({
  departments: Yup.array().of(
    Yup.object({
      title: Yup.string().required(),
      employees: Yup.array().of(
        Yup.object({
          name: Yup.string().required(),
          position: Yup.string()
        })
      )
    })
  )
});

Здесь формируется иерархия:

  • departments

    • employees

      • поля сотрудника

Каждый уровень имеет собственную схему валидации, независимую от других уровней.

Условная вложенная валидация

Для управления поведением вложенных схем используется when, позволяющий изменять правила в зависимости от значения других полей.

const schema = Yup.object({
  hasAddress: Yup.boolean(),
  address: Yup.object().when('hasAddress', {
    is: true,
    then: Yup.object({
      city: Yup.string().required(),
      street: Yup.string().required()
    }),
    otherwise: Yup.object().nullable()
  })
});

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

Динамические схемы через lazy

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

const schema = Yup.lazy(value => {
  if (Array.isArray(value)) {
    return Yup.array().of(Yup.string());
  }

  return Yup.object({
    type: Yup.string(),
    value: Yup.mixed()
  });
});

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

Ошибки во вложенных структурах

Yup возвращает ошибки в виде структурированного объекта, где каждая ошибка привязана к конкретному пути.

Пример результата:

{
  "users[0].name": "Name is required",
  "users[1].age": "Must be greater than 18"
}

Такая структура позволяет точно сопоставить ошибку с элементом интерфейса.

Объединение схем через concat

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

const baseSchema = Yup.object({
  name: Yup.string().required()
});

const extendedSchema = baseSchema.concat(
  Yup.object({
    age: Yup.number().required()
  })
);

Этот механизм часто используется при построении модульных схем.

Вложенные mixed-типы

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

const schema = Yup.object({
  config: Yup.mixed().test(
    'is-valid-config',
    'Invalid config',
    value => typeof value === 'object'
  )
});

Хотя такой подход снижает строгость типизации, он необходим при работе с динамическими данными.

Рекурсивные схемы

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

const nodeSchema = Yup.object({
  name: Yup.string().required(),
  children: Yup.array().of(Yup.lazy(() => nodeSchema))
});

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

Вложенные схемы и производительность

Глубина вложенности напрямую влияет на стоимость валидации. Каждое дополнительное вложение добавляет уровень обхода структуры данных. При работе с большими объектами рекомендуется:

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

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