Вложенные массивы

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


Массив массивов

Простейший вариант вложенности — массив, содержащий другие массивы.

const Joi = require('joi');

const schema = Joi.array().items(
  Joi.array().items(Joi.number())
);

const data = [
  [1, 2, 3],
  [4, 5],
  [10, 20, 30]
];

const result = schema.validate(data);

console.log(result.error);

Схема:

Joi.array().items(
  Joi.array().items(Joi.number())
)

означает:

  • внешний уровень — массив;
  • каждый элемент внешнего массива — тоже массив;
  • внутренние элементы — числа.

Ограничения для внутренних массивов

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

const schema = Joi.array().items(
  Joi.array()
    .items(Joi.number().integer())
    .min(2)
    .max(5)
);

Здесь:

  • каждый вложенный массив содержит только целые числа;
  • минимум 2 элемента;
  • максимум 5 элементов.

Пример валидных данных:

[
  [1, 2],
  [10, 20, 30],
  [5, 6, 7, 8]
]

Пример невалидных данных:

[
  [1],
  [2, 3, 4, 5, 6, 7]
]

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

Наиболее распространённый сценарий — объект с полем-массивом.

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

  tags: Joi.array()
    .items(Joi.string())
    .min(1)
    .required()
});

Пример данных:

const data = {
  title: 'Node.js',
  tags: ['backend', 'javascript', 'api']
};

Глубокая вложенность

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

const schema = Joi.array().items(
  Joi.object({
    category: Joi.string().required(),

    products: Joi.array().items(
      Joi.object({
        name: Joi.string().required(),

        prices: Joi.array().items(
          Joi.number().positive()
        )
      })
    )
  })
);

Структура данных:

[
  {
    category: 'Phones',

    products: [
      {
        name: 'iPhone',
        prices: [999, 1099]
      },

      {
        name: 'Samsung',
        prices: [899]
      }
    ]
  }
]

Здесь присутствует несколько уровней:

  1. массив категорий;
  2. объект категории;
  3. массив товаров;
  4. объект товара;
  5. массив цен.

Вложенные массивы с обязательными полями

Валидация может контролировать обязательность элементов на каждом уровне.

const schema = Joi.object({
  users: Joi.array()
    .items(
      Joi.object({
        name: Joi.string().required(),

        skills: Joi.array()
          .items(Joi.string().required())
          .required()
      })
    )
    .required()
});

Особенности:

  • users обязан существовать;
  • каждый объект пользователя обязан содержать skills;
  • внутри skills допускаются только строки;
  • пустые значения запрещены.

Проверка пустых вложенных массивов

По умолчанию массив может быть пустым.

Joi.array()

Чтобы запретить пустой массив:

Joi.array().min(1)

Пример:

const schema = Joi.object({
  matrix: Joi.array()
    .items(
      Joi.array()
        .items(Joi.number())
        .min(1)
    )
    .min(1)
});

Такая схема запрещает:

[]

и:

[
  []
]

Многомерные массивы

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

Двумерный массив

const schema = Joi.array().items(
  Joi.array().items(Joi.number())
);

Пример:

[
  [1, 2],
  [3, 4]
]

Трёхмерный массив

const schema = Joi.array().items(
  Joi.array().items(
    Joi.array().items(Joi.number())
  )
);

Пример:

[
  [
    [1, 2],
    [3, 4]
  ],

  [
    [5, 6]
  ]
]

Вложенные массивы фиксированной структуры

Иногда требуется строго фиксированная форма массива.

const schema = Joi.array().ordered(
  Joi.string(),
  Joi.array().items(Joi.number())
);

Ожидаемая структура:

[
  'coordinates',
  [10, 20, 30]
]

Если порядок нарушится — валидация завершится ошибкой.


Комбинация ordered() и items()

Методы ordered() и items() могут использоваться одновременно.

const schema = Joi.array()
  .ordered(
    Joi.string(),
    Joi.number()
  )
  .items(Joi.boolean());

Допустимые данные:

[
  'start',
  100,
  true,
  false,
  true
]

Первые два элемента имеют фиксированный тип, остальные проверяются через items().


Вложенные массивы с уникальными значениями

Метод unique() работает и для внутренних массивов.

const schema = Joi.array().items(
  Joi.array()
    .items(Joi.number())
    .unique()
);

Допустимо:

[
  [1, 2, 3],
  [10, 20]
]

Ошибка:

[
  [1, 1, 2]
]

Проверка длины внутренних массивов

const schema = Joi.array().items(
  Joi.array()
    .length(3)
    .items(Joi.number())
);

Каждый вложенный массив обязан содержать ровно 3 элемента.

Пример:

[
  [1, 2, 3],
  [4, 5, 6]
]

Использование single()

Метод single() позволяет автоматически преобразовывать одиночное значение в массив.

const schema = Joi.array()
  .items(
    Joi.array()
      .items(Joi.string())
      .single()
  );

Пример:

[
  'admin',
  ['editor', 'moderator']
]

После преобразования:

[
  ['admin'],
  ['editor', 'moderator']
]

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

Метод alternatives() помогает описывать сложные структуры.

const schema = Joi.array().items(
  Joi.alternatives().try(
    Joi.array().items(Joi.string()),

    Joi.array().items(Joi.number())
  )
);

Допустимо:

[
  ['a', 'b'],
  [1, 2, 3]
]

Недопустимо:

[
  ['a', 1]
]

Массив объектов с вложенными дочерними элементами

Часто используется древовидная структура.

const categorySchema = Joi.object({
  name: Joi.string().required(),

  children: Joi.array().items(
    Joi.object({
      name: Joi.string().required()
    })
  )
});

Пример:

{
  name: 'Programming',

  children: [
    { name: 'JavaScript' },
    { name: 'Python' }
  ]
}

Рекурсивные вложенные массивы

Для рекурсивных структур используется link().

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

  children: Joi.array().items(
    Joi.link('#node')
  )
}).id('node');

Пример:

{
  name: 'Root',

  children: [
    {
      name: 'Child 1',

      children: [
        {
          name: 'Nested Child',
          children: []
        }
      ]
    }
  ]
}

Кастомная проверка вложенных массивов

Метод custom() позволяет создавать собственные правила.

const schema = Joi.array().items(
  Joi.array().custom((value, helpers) => {

    const sum = value.reduce((a, b) => a + b, 0);

    if (sum > 100) {
      return helpers.error('array.sumExceeded');
    }

    return value;
  })
);

Создание собственного сообщения:

const schema = Joi.array().items(
  Joi.array()
    .custom((value, helpers) => {

      const sum = value.reduce((a, b) => a + b, 0);

      if (sum > 100) {
        return helpers.error('array.sumExceeded');
      }

      return value;
    })

    .messages({
      'array.sumExceeded': 'Сумма элементов массива превышает 100'
    })
);

Валидация вложенных массивов с unknown()

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

const schema = Joi.array().items(
  Joi.object({
    id: Joi.number().required()
  }).unknown(true)
);

Пример:

[
  {
    id: 1,
    extra: 'value'
  }
]

Без unknown(true) поле extra вызвало бы ошибку.


Ошибки вложенных массивов

Joi формирует подробные пути ошибок.

const schema = Joi.array().items(
  Joi.object({
    scores: Joi.array().items(
      Joi.number().min(0)
    )
  })
);

Невалидные данные:

[
  {
    scores: [10, -5]
  }
]

Ошибка:

[
  {
    message: '"[0].scores[1]" must be greater than or equal to 0',
    path: [0, 'scores', 1]
  }
]

Путь ошибки показывает:

  • объект №0;
  • поле scores;
  • элемент массива №1.

abortEarly и вложенные массивы

По умолчанию Joi останавливается после первой ошибки.

schema.validate(data);

Чтобы получить все ошибки:

schema.validate(data, {
  abortEarly: false
});

Пример:

const schema = Joi.array().items(
  Joi.array().items(
    Joi.number().min(10)
  )
);

const data = [
  [1, 2],
  [3, 4]
];

Результат:

[
  '"[0][0]" must be greater than or equal to 10',
  '"[0][1]" must be greater than or equal to 10',
  '"[1][0]" must be greater than or equal to 10',
  '"[1][1]" must be greater than or equal to 10'
]

Преобразование данных во вложенных массивах

Joi умеет автоматически конвертировать значения.

const schema = Joi.array().items(
  Joi.array().items(
    Joi.number()
  )
);

Данные:

[
  ['1', '2'],
  ['3']
]

После валидации:

[
  [1, 2],
  [3]
]

Отключение преобразования:

schema.validate(data, {
  convert: false
});

Использование sparse()

Метод sparse() разрешает undefined внутри массива.

const schema = Joi.array()
  .sparse()
  .items(
    Joi.array().items(Joi.number())
  );

Пример:

[
  undefined,
  [1, 2]
]

Без sparse() значение undefined вызовет ошибку.


Сложная структура API

Пример схемы для REST API.

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

  items: Joi.array().items(
    Joi.object({
      id: Joi.number().required(),

      title: Joi.string().required(),

      comments: Joi.array().items(
        Joi.object({
          author: Joi.string().required(),

          messages: Joi.array().items(
            Joi.string().min(1)
          )
        })
      )
    })
  )
});

Такая схема проверяет:

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