required() и фактической логикой приложенияОдна из самых распространённых проблем — несогласованность схемы
валидации с бизнес-логикой. Поле объявляется обязательным через
required(), хотя на практике может отсутствовать.
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().required(),
avatar: Joi.string().required()
});
Если avatar действительно необязателен, схема начинает
выбрасывать ошибки в корректных сценариях.
Правильный вариант:
const schema = Joi.object({
username: Joi.string().required(),
avatar: Joi.string().optional()
});
Либо:
avatar: Joi.string().allow(null)
undefined и null — разные значенияМногие ожидают, что optional() автоматически разрешает
null.
Это неверно.
const schema = Joi.object({
age: Joi.number().optional()
});
Допустимо:
{}
Недопустимо:
{
age: null
}
Для поддержки null требуется:
const schema = Joi.object({
age: Joi.number().allow(null)
});
Комбинация:
age: Joi.number().optional().allow(null)
означает:
null;Joi автоматически конвертирует значения.
Пример:
const schema = Joi.object({
age: Joi.number()
});
schema.validate({
age: '25'
});
Строка '25' превратится в число 25.
Иногда это полезно, но может приводить к скрытым ошибкам.
schema.validate(data, {
convert: false
});
Теперь:
{
age: '25'
}
вызовет ошибку:
"age" must be a number
const schema = Joi.object({
isAdmin: Joi.boolean()
});
Joi автоматически преобразует:
'true' -> true
'false' -> false
При строгой типизации лучше отключать convert.
Joi.string()
разрешает:
''
Это часто становится причиной багов.
Joi.string().min(1)
или:
Joi.string().empty('')
Пример:
const schema = Joi.object({
username: Joi.string().min(3).required()
});
allow()allow('') ломает
ограниченияПример:
Joi.string().min(5).allow('')
Пустая строка теперь полностью проходит валидацию, несмотря на
min(5).
Это связано с тем, что allow() добавляет значение в
список допустимых исключений.
По умолчанию Joi отклоняет лишние поля.
const schema = Joi.object({
username: Joi.string()
});
Ошибка:
{
username: 'alex',
role: 'admin'
}
Сообщение:
"role" is not allowed
Joi.object({
username: Joi.string()
}).unknown(true)
unknown(true)Разрешение любых полей может создавать уязвимости.
Опасный пример:
{
username: 'alex',
isAdmin: true
}
Если объект напрямую сохраняется в БД, злоумышленник может передать неожиданные свойства.
Безопаснее явно описывать структуру.
Плохой пример:
const schema = Joi.object({
profile: Joi.object()
});
Такой объект пропускает практически любые данные.
Правильный вариант:
const schema = Joi.object({
profile: Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required()
})
});
Joi.array()
Такой массив принимает любые значения.
Joi.array().items(Joi.string())
Пример:
const schema = Joi.object({
tags: Joi.array().items(Joi.string())
});
Joi.array()
разрешает:
[]
Joi.array().min(1)
valid()valid()
делает список строго ограниченнымJoi.string().valid('admin', 'user')
Теперь любые другие значения запрещены.
Ошибка возникает, когда разработчик ожидает лишь рекомендацию, а получает жёсткое ограничение.
Joi.date()
валидирует:
'2025-01-01'
new Date('2025-01-01')
может интерпретироваться по-разному в зависимости от окружения.
Joi.date().iso()
Неверно:
const schema = Joi.string().custom((value, helpers) => {
if (value.includes('admin')) {
helpers.error('string.invalid');
}
});
Ошибка: валидатор не возвращает значение.
Правильно:
const schema = Joi.string().custom((value, helpers) => {
if (value.includes('admin')) {
return helpers.error('string.invalid');
}
return value;
});
messages()Joi.string().messages({
required: 'Поле обязательно'
});
Так работать не будет.
Нужны полные коды ошибок:
Joi.string().messages({
'any.required': 'Поле обязательно'
});
error.detailsconst { error } = schema.validate(data);
if (error) {
console.log(error.details[0].message);
}
Проблема: остальные ошибки теряются.
const messages = error.details.map(item => item.message);
По умолчанию Joi останавливается после первой ошибки.
schema.validate(data);
schema.validate(data, {
abortEarly: false
});
when()Ошибка:
Joi.string().when('role', {
is: 'admin',
then: Joi.required()
});
Проблема возникает, если role отсутствует или имеет
другой тип.
const schema = Joi.object({
role: Joi.string().required(),
accessCode: Joi.string().when('role', {
is: 'admin',
then: Joi.required(),
otherwise: Joi.optional()
})
});
default()const schema = Joi.object({
role: Joi.string().default('user')
});
После валидации:
const result = schema.validate({});
Значение будет находиться здесь:
result.value
а не в исходном объекте.
const base = Joi.string();
const requiredField = base.required();
Joi создаёт новую схему, а не мутирует старую.
Иногда разработчики ожидают обратное.
Joi.string().email()
проверяет только формат.
Это не означает:
Даже если Joi используется во frontend-приложении:
const schema = Joi.object({
username: Joi.string().required()
});
сервер обязан выполнять повторную проверку.
Клиентская валидация никогда не считается безопасной.
Частая проблема — использование одной схемы:
const userSchema = Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required()
});
Для PATCH-запросов такая схема неудобна.
fork()const updateSchema = userSchema.fork(
['username', 'email'],
field => field.optional()
);
Плохой пример:
app.post('/users', (req, res) => {
const schema = Joi.object({
username: Joi.string().required()
});
schema.validate(req.body);
});
Схема создаётся при каждом запросе.
const schema = Joi.object({
username: Joi.string().required()
});
app.post('/users', (req, res) => {
schema.validate(req.body);
});
validate() вместо validateAsync()Если присутствуют асинхронные правила:
await schema.validateAsync(data);
иначе возможны непредсказуемые ошибки.
strip()password: Joi.string().strip()
Поле будет удалено из результата.
Это полезно для скрытия служебных данных, но может неожиданно ломать дальнейшую логику.
Некоторые старые примеры используют:
Joi.validate(data, schema);
В современных версиях:
schema.validate(data);
Ошибка:
schema.validate(req.body);
next();
Валидация выполняется, но результат не проверяется.
Правильно:
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: error.details
});
}
req.body = value;
next();
number() принимает
InfinityНеочевидное поведение:
Joi.number()
может пропускать специальные числовые значения.
Joi.number().min(0).max(100)
regex()Плохой пример:
Joi.string().pattern(/^(?=.*[A-Z])(?=.*\d).+$/)
Через несколько месяцев такой код становится трудно поддерживать.
const PASSWORD_REGEX =
/^(?=.*[A-Z])(?=.*\d).+$/;
const schema = Joi.string().pattern(PASSWORD_REGEX);
Joi.string().min(8)
не обеспечивает безопасность.
Минимально полезная схема:
Joi.string()
.min(8)
.pattern(/[A-Z]/)
.pattern(/[a-z]/)
.pattern(/[0-9]/)
alternatives()Joi.alternatives().try(
Joi.string(),
Joi.number()
)
Иногда значения неожиданно проходят из-за автоматической конвертации.
Пример:
'123'
может быть воспринято как число.
schema.validate(data, {
convert: false
});
Плохой подход:
const schema = Joi.object({
// сотни полей
});
Поддержка таких схем быстро усложняется.
const addressSchema = Joi.object({
city: Joi.string(),
street: Joi.string()
});
const userSchema = Joi.object({
username: Joi.string(),
address: addressSchema
});
Даже простая схема может содержать ошибки:
Joi.string().email()
Без тестов сложно гарантировать корректность поведения.
Пример теста:
expect(schema.validate({
email: 'wrong'
}).error).toBeDefined();
Joi.object({
username: Joi.string().required()
})
.required()
.unknown(false);
schemas/
user.schema.js
auth.schema.js
product.schema.js
const emailRule = Joi.string().email().required();
schema.validate(data, {
abortEarly: false,
convert: false
});
createUserSchema
updateUserSchema
Joi должен использоваться: