Валидация вложенных массивов — одна из наиболее востребованных задач при работе с 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)
);
Здесь:
Пример валидных данных:
[
[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]
}
]
}
]
Здесь присутствует несколько уровней:
Валидация может контролировать обязательность элементов на каждом уровне.
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() могут
использоваться одновременно.
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() позволяет автоматически преобразовывать
одиночное значение в массив.
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'
})
);
Иногда объекты внутри массива могут содержать дополнительные поля.
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]
}
]
Путь ошибки показывает:
scores;По умолчанию 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() разрешает undefined внутри
массива.
const schema = Joi.array()
.sparse()
.items(
Joi.array().items(Joi.number())
);
Пример:
[
undefined,
[1, 2]
]
Без sparse() значение undefined вызовет
ошибку.
Пример схемы для 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)
)
})
)
})
)
});
Такая схема проверяет: