В библиотеке Joi объектная валидация строится вокруг схем. Любая
структура данных описывается через специальные schema-объекты, а
ключевую роль в работе со сложными объектами играют методы
keys() и механизм schema-композиции.
keys() используется для описания структуры объекта,
определения допустимых полей, их типов, ограничений и поведения при
валидации.
const Joi = require('joi');
const schema = Joi.object().keys({
username: Joi.string().min(3).max(30).required(),
age: Joi.number().integer().min(18),
email: Joi.string().email()
});
В этом примере:
username — обязательная строка;age — целое число не меньше 18;email — строка в формате email.keys() принимает объект, где:
const userSchema = Joi.object().keys({
id: Joi.number(),
name: Joi.string(),
active: Joi.boolean()
});
Аналогичная запись:
const userSchema = Joi.object({
id: Joi.number(),
name: Joi.string(),
active: Joi.boolean()
});
Обе формы равнозначны.
const schema = Joi.object({
title: Joi.string().required(),
price: Joi.number().positive().required()
});
const result = schema.validate({
title: 'Ноутбук',
price: 500
});
console.log(result.error);
Если данные корректны:
null
Если есть ошибка:
"price" must be a positive number
const schema = Joi.object({
login: Joi.string().required(),
password: Joi.string().required()
});
Без обязательного поля:
{
login: 'admin'
}
Ошибка:
"password" is required
Все поля по умолчанию необязательные.
const schema = Joi.object({
middleName: Joi.string().optional()
});
Запрещённое поле:
const schema = Joi.object({
role: Joi.forbidden()
});
При передаче:
{
role: 'admin'
}
Возникнет ошибка.
Минимальное количество свойств:
const schema = Joi.object({
name: Joi.string(),
age: Joi.number()
}).min(1);
Объект должен содержать минимум одно поле.
const schema = Joi.object({
a: Joi.any(),
b: Joi.any(),
c: Joi.any()
}).max(2);
Точное количество ключей:
const schema = Joi.object({
x: Joi.any(),
y: Joi.any()
}).length(2);
Запрет дополнительных полей:
const schema = Joi.object({
username: Joi.string()
}).unknown(false);
Проверка:
schema.validate({
username: 'alex',
role: 'admin'
});
Ошибка:
"role" is not allowed
Разрешение произвольных свойств:
const schema = Joi.object({
id: Joi.number()
}).unknown(true);
Теперь дополнительные поля допустимы.
const schema = Joi.object({
user: Joi.object({
profile: Joi.object({
firstName: Joi.string(),
lastName: Joi.string()
})
})
});
Проверяемый объект:
{
user: {
profile: {
firstName: 'Ivan',
lastName: 'Petrov'
}
}
}
const addressSchema = Joi.object({
city: Joi.string().required(),
street: Joi.string().required(),
house: Joi.string().required()
});
const userSchema = Joi.object({
name: Joi.string(),
address: addressSchema
});
Повторное использование схем делает код:
Добавление новых ключей в существующую схему.
const baseSchema = Joi.object({
name: Joi.string()
});
const extendedSchema = baseSchema.append({
age: Joi.number()
});
Теперь схема содержит:
{
name,
age
}
Получение схемы отдельного ключа.
const schema = Joi.object({
username: Joi.string().min(5),
password: Joi.string().min(8)
});
const usernameSchema = schema.extract('username');
Проверка:
usernameSchema.validate('admin');
Изменение части схемы.
const schema = Joi.object({
username: Joi.string(),
email: Joi.string(),
password: Joi.string()
});
const requiredSchema = schema.fork(
['username', 'password'],
field => field.required()
);
Теперь username и password обязательны.
Переименование ключей.
const schema = Joi.object({
username: Joi.string()
}).rename('login', 'username');
Проверка:
schema.validate({
login: 'admin'
});
После обработки:
{
username: 'admin'
}
Проверка динамических ключей.
const schema = Joi.object().pattern(
/^item_/,
Joi.number()
);
Допустимый объект:
{
item_1: 10,
item_2: 20
}
Недопустимый:
{
item_1: 'text'
}
Объединение схем.
const schemaA = Joi.object({
name: Joi.string()
});
const schemaB = Joi.object({
age: Joi.number()
});
const merged = schemaA.concat(schemaB);
Результат:
{
name,
age
}
Позволяет использовать разные варианты структуры.
const schema = Joi.alternatives().try(
Joi.string(),
Joi.number()
);
Допустимы:
'hello'
100
const schema = Joi.object({
type: Joi.string().required(),
value: Joi.when('type', {
is: 'number',
then: Joi.number().required(),
otherwise: Joi.string().required()
})
});
Проверка:
{
type: 'number',
value: 10
}
Если существует один ключ — требуется другой.
const schema = Joi.object({
password: Joi.string(),
repeatPassword: Joi.string()
}).with('password', 'repeatPassword');
Запрет совместного использования.
const schema = Joi.object({
token: Joi.string(),
password: Joi.string()
}).without('token', 'password');
Разрешён только один ключ.
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string()
}).xor('email', 'phone');
Допустимо:
{
email: 'test@mail.com'
}
Недопустимо:
{
email: 'a@mail.com',
phone: '+123'
}
Все или ничего.
const schema = Joi.object({
startDate: Joi.date(),
endDate: Joi.date()
}).and('startDate', 'endDate');
Хотя бы одно поле.
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string(),
telegram: Joi.string()
}).or('email', 'phone', 'telegram');
Использование значения другого поля.
const schema = Joi.object({
password: Joi.string().required(),
repeatPassword: Joi.any()
.valid(Joi.ref('password'))
.required()
});
Проверка:
{
password: '123456',
repeatPassword: '123456'
}
Вывод структуры схемы.
const schema = Joi.object({
username: Joi.string().required()
});
console.log(schema.describe());
Полезно для:
Добавление произвольной информации.
const schema = Joi.string().meta({
uiType: 'input'
});
const schema = Joi.string().note('Поле используется для авторизации');
const schema = Joi.string().tag('auth');
Настройка параметров валидации.
const schema = Joi.object({
age: Joi.number()
}).prefs({
abortEarly: false
});
Остановка на первой ошибке.
Сбор всех ошибок.
Пример:
const schema = Joi.object({
username: Joi.string().min(5).required(),
age: Joi.number().min(18)
});
const result = schema.validate(
{
username: 'ab',
age: 10
},
{
abortEarly: false
}
);
console.log(result.error.details);
Удаление поля после проверки.
const schema = Joi.object({
username: Joi.string(),
password: Joi.string().strip()
});
Результат:
{
username: 'admin'
}
Значение по умолчанию.
const schema = Joi.object({
role: Joi.string().default('user')
});
Проверка:
schema.validate({});
Результат:
{
role: 'user'
}
По умолчанию Joi преобразует значения.
const schema = Joi.number();
schema.validate('100');
Результат:
100
const schema = Joi.number().prefs({
convert: false
});
Теперь строка "100" вызовет ошибку.
const schema = Joi.string().custom((value, helpers) => {
if (value.includes('admin')) {
return helpers.error('string.invalid');
}
return value;
});
const schema = Joi.object({
username: Joi.string().required()
});
async function run() {
try {
const result = await schema.validateAsync({
username: 'alex'
});
console.log(result);
} catch (err) {
console.log(err);
}
}
const Joi = require('joi');
const addressSchema = Joi.object({
city: Joi.string().required(),
street: Joi.string().required(),
zip: Joi.string().pattern(/^\d{6}$/)
});
const userSchema = Joi.object({
id: Joi.number()
.integer()
.positive(),
username: Joi.string()
.alphanum()
.min(3)
.max(20)
.required(),
email: Joi.string()
.email()
.required(),
age: Joi.number()
.min(18),
roles: Joi.array()
.items(Joi.string())
.default(['user']),
address: addressSchema.required(),
password: Joi.string()
.min(8)
.required(),
repeatPassword: Joi.any()
.valid(Joi.ref('password'))
.required()
}).with('password', 'repeatPassword');
Хорошая практика:
schemas/
user.schema.js
auth.schema.js
product.schema.js
const idSchema = Joi.number()
.integer()
.positive();
const productSchema = Joi.object({
id: idSchema,
title: Joi.string()
});
Проблема:
Joi.object({...}).unknown(true)
Может пропускать:
Joi.object({
email: Joi.string().email()
})
Поле необязательное, хотя часто предполагается обратное.
Плохо:
const schema = Joi.object({
...
// сотни строк
});
Лучше:
Крупные проекты обычно используют:
Пример структуры:
src/
validation/
user/
auth/
product/
Схема Joi фактически становится контрактом между:
Благодаря keys() и schema-композиции можно: