Валидация структурированных данных в JavaScript часто требует работы с объектами, содержащими другие объекты произвольной глубины. Библиотека Joi предоставляет гибкий механизм описания таких структур через композицию схем, позволяя точно определять правила для каждого уровня вложенности.
В Joi объект описывается через Joi.object(), а его
структура задаётся методом keys():
const Joi = require('joi');
const schema = Joi.object({
user: Joi.object({
name: Joi.string().min(2).max(50).required(),
age: Joi.number().integer().min(0).max(120)
})
});
В данном примере поле user является вложенным объектом,
содержащим два поля с собственными правилами валидации.
Вложенные объекты подчиняются тем же правилам, что и обычные поля.
Обязательность задаётся через required():
const schema = Joi.object({
profile: Joi.object({
username: Joi.string().alphanum().required(),
bio: Joi.string().max(200).optional()
}).required()
});
Если объект profile отсутствует, валидация завершится
ошибкой. При этом поле bio может отсутствовать без
нарушения правил.
Joi поддерживает произвольную глубину структур, включая вложенные объекты внутри вложенных объектов:
const schema = Joi.object({
user: Joi.object({
name: Joi.string().required(),
contact: Joi.object({
email: Joi.string().email().required(),
phone: Joi.object({
countryCode: Joi.string().required(),
number: Joi.string().pattern(/^[0-9]+$/).required()
})
})
})
});
Здесь структура имеет три уровня вложенности, каждый из которых валидируется независимо.
Схемы Joi можно выносить в отдельные переменные и использовать повторно, что особенно важно при работе со сложными вложенными структурами:
const phoneSchema = Joi.object({
countryCode: Joi.string().required(),
number: Joi.string().required()
});
const contactSchema = Joi.object({
email: Joi.string().email().required(),
phone: phoneSchema
});
const userSchema = Joi.object({
name: Joi.string().required(),
contact: contactSchema
});
Такой подход уменьшает дублирование и упрощает поддержку.
Часто вложенные объекты комбинируются с массивами:
const schema = Joi.object({
users: Joi.array().items(
Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
address: Joi.object({
city: Joi.string().required(),
zip: Joi.string().required()
})
})
)
});
Каждый элемент массива проходит валидацию по одинаковой вложенной схеме.
В некоторых случаях требуется разрешить дополнительные поля внутри вложенных объектов:
const schema = Joi.object({
settings: Joi.object({
theme: Joi.string().valid('dark', 'light'),
notifications: Joi.object({
email: Joi.boolean(),
sms: Joi.boolean()
}).unknown(true)
})
});
Метод unknown(true) разрешает наличие дополнительных
ключей, не описанных в схеме.
Метод keys() используется для явного определения
структуры объекта:
const addressSchema = Joi.object().keys({
street: Joi.string().required(),
city: Joi.string().required(),
country: Joi.string().required()
});
Вложение этой схемы в другой объект остаётся прозрачным:
const schema = Joi.object({
user: Joi.object({
name: Joi.string().required(),
address: addressSchema
})
});
Joi позволяет добавлять условные правила даже внутри вложенных структур:
const schema = Joi.object({
payment: Joi.object({
method: Joi.string().valid('card', 'cash').required(),
cardDetails: Joi.object({
number: Joi.string().creditCard().required(),
cvv: Joi.string().required()
}).when('method', {
is: 'card',
then: Joi.required(),
otherwise: Joi.forbidden()
})
})
});
Такой подход позволяет динамически изменять требования к вложенным объектам.
Схемы можно расширять без изменения оригинала:
const baseUser = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
});
const extendedUser = baseUser.keys({
role: Joi.string().valid('admin', 'user')
});
Расширенная схема сохраняет структуру исходной и добавляет новые поля.
Для сложных моделей данных иногда требуется альтернативная структура вложенного объекта:
const schema = Joi.object({
config: Joi.alternatives().try(
Joi.object({
mode: Joi.string().valid('simple')
}),
Joi.object({
mode: Joi.string().valid('advanced'),
options: Joi.object({
retries: Joi.number(),
timeout: Joi.number()
})
})
)
});
Каждая альтернатива может содержать собственную вложенную структуру.
Часто встречаются ошибки, связанные с неполным описанием структуры:
const schema = Joi.object({
user: {
name: Joi.string()
}
});
В этом случае отсутствует Joi.object(), из-за чего
вложенность не распознаётся корректно. Правильный вариант:
const schema = Joi.object({
user: Joi.object({
name: Joi.string()
})
});
При проектировании схем сложных данных удобно использовать композицию:
const metadataSchema = Joi.object({
createdAt: Joi.date().required(),
updatedAt: Joi.date().required()
});
const articleSchema = Joi.object({
title: Joi.string().required(),
content: Joi.string().required(),
metadata: metadataSchema
});
Такой подход позволяет формировать повторно используемые блоки вложенных объектов и собирать из них более сложные структуры без дублирования логики.