По умолчанию Ajv возвращает массив ошибок в свойстве
validate.errors. Каждая ошибка содержит техническую
информацию:
message.Пример стандартной ошибки:
[
{
instancePath: "/age",
schemaPath: "#/properties/age/minimum",
keyword: "minimum",
params: {
comparison: ">=",
limit: 18
},
message: "must be >= 18"
}
]
Для внутренних сервисов такого формата часто достаточно. Однако в пользовательских API, формах регистрации, административных панелях и публичных REST-интерфейсах обычно требуются:
Именно для этого используются кастомные error handler.
Каждая ошибка Ajv содержит набор полей.
{
keyword: "required",
instancePath: "",
schemaPath: "#/required",
params: {
missingProperty: "email"
},
message: "must have required property 'email'"
}
| Поле | Назначение |
|---|---|
keyword |
Тип ошибки |
instancePath |
Путь к данным |
schemaPath |
Путь к правилу схемы |
params |
Дополнительные параметры |
message |
Текст ошибки |
const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
type: "object",
required: ["email"],
properties: {
email: {
type: "string",
format: "email"
}
}
}
const validate = ajv.compile(schema)
const data = {}
const valid = validate(data)
if (!valid) {
const errors = validate.errors.map(error => ({
field: error.instancePath,
type: error.keyword,
message: error.message
}))
console.log(errors)
}
Результат:
[
{
field: "",
type: "required",
message: "must have required property 'email'"
}
]
function formatError(error) {
switch (error.keyword) {
case "required":
return "Обязательное поле отсутствует"
case "type":
return "Неверный тип данных"
case "minimum":
return "Значение слишком маленькое"
default:
return "Ошибка валидации"
}
}
Использование:
const formatted = validate.errors.map(error => ({
field: error.instancePath,
message: formatError(error)
}))
Поле params содержит дополнительную информацию о
нарушении.
{
keyword: "minimum",
params: {
comparison: ">=",
limit: 18
}
}
Формирование сообщения:
function formatError(error) {
switch (error.keyword) {
case "minimum":
return `Минимальное значение: ${error.params.limit}`
case "maximum":
return `Максимальное значение: ${error.params.limit}`
default:
return error.message
}
}
Для required путь к отсутствующему полю хранится
отдельно.
{
keyword: "required",
instancePath: "",
params: {
missingProperty: "email"
}
}
Получение имени поля:
function formatRequired(error) {
return `Поле ${error.params.missingProperty} обязательно`
}
function formatAjvErrors(errors) {
return errors.map(error => {
let message
switch (error.keyword) {
case "required":
message =
`Поле ${error.params.missingProperty} обязательно`
break
case "type":
message =
`Ожидается тип ${error.params.type}`
break
case "minLength":
message =
`Минимальная длина: ${error.params.limit}`
break
case "minimum":
message =
`Минимальное значение: ${error.params.limit}`
break
default:
message = error.message
}
return {
field: error.instancePath || "/",
code: error.keyword,
message
}
})
}
app.post("/users", (req, res) => {
const valid = validate(req.body)
if (!valid) {
return res.status(400).json({
status: "error",
errors: formatAjvErrors(validate.errors)
})
}
res.json({
status: "ok"
})
})
Ответ:
{
"status": "error",
"errors": [
{
"field": "/age",
"code": "minimum",
"message": "Минимальное значение: 18"
}
]
}
Ajv использует JSON Pointer:
/address/street
Во многих приложениях требуется:
address.street
function normalizePath(path) {
return path
.replace(/\//g, ".")
.replace(/^\./, "")
}
Использование:
const field = normalizePath(error.instancePath)
Результат:
address.street
Многие UI-библиотеки ожидают:
{
email: ["Неверный email"],
password: ["Слишком короткий пароль"]
}
function groupErrors(errors) {
return errors.reduce((acc, error) => {
const field = normalizePath(error.instancePath)
if (!acc[field]) {
acc[field] = []
}
acc[field].push(error.message)
return acc
}, {})
}
const schema = {
type: "object",
properties: {
profile: {
type: "object",
properties: {
age: {
type: "integer",
minimum: 18
}
}
}
}
}
Ошибка:
{
instancePath: "/profile/age",
keyword: "minimum"
}
После нормализации:
profile.age
const schema = {
type: "array",
items: {
type: "string",
minLength: 3
}
}
Ошибка:
{
instancePath: "/0",
keyword: "minLength"
}
Для вложенных структур:
/users/0/email
Преобразование:
users.0.email
Текст сообщения может меняться:
Код ошибки должен оставаться стабильным.
function buildError(error) {
const map = {
required: "ERR_REQUIRED",
type: "ERR_INVALID_TYPE",
minimum: "ERR_MINIMUM",
format: "ERR_INVALID_FORMAT"
}
return {
code: map[error.keyword] || "ERR_VALIDATION",
message: error.message
}
}
const messages = {
required: "Поле обязательно",
type: "Неверный тип",
minimum: "Слишком маленькое значение"
}
function translate(error) {
return messages[error.keyword]
}
Для автоматической локализации используется пакет:
npm install ajv-i18n
const Ajv = require("ajv")
const localize = require("ajv-i18n")
const ajv = new Ajv()
const validate = ajv.compile(schema)
validate(data)
if (validate.errors) {
localize.ru(validate.errors)
console.log(validate.errors)
}
После локализации:
[
{
message: "должно быть не меньше 18"
}
]
npm install ajv-errors
const Ajv = require("ajv")
const ajvErrors = require("ajv-errors")
const ajv = new Ajv({
allErrors: true
})
ajvErrors(ajv)
const schema = {
type: "object",
required: ["email"],
properties: {
email: {
type: "string",
format: "email"
}
},
errorMessage: {
required: {
email: "Email обязателен"
},
properties: {
email: "Некорректный email"
}
}
}
Без ajv-errors:
must have required property 'email'
С ajv-errors:
Email обязателен
const schema = {
type: "object",
properties: {
password: {
type: "string",
minLength: 8
}
},
errorMessage: {
properties: {
password:
"Пароль должен содержать минимум 8 символов"
}
}
}
const schema = {
type: "string",
minLength: 5,
errorMessage: {
type: "Должна быть строка",
minLength: "Минимум 5 символов"
}
}
const schema = {
type: "object",
properties: {
age: {
type: "number",
minimum: 18
}
},
errorMessage:
"Данные пользователя заполнены некорректно"
}
По умолчанию Ajv останавливается после первой ошибки.
const ajv = new Ajv({
allErrors: true
})
Теперь собираются все ошибки.
Это особенно важно для:
const schema = {
type: "string",
format: "email"
}
Ошибка:
{
keyword: "format",
params: {
format: "email"
}
}
Кастомизация:
function formatError(error) {
if (error.keyword === "format") {
return `Некорректный формат: ${error.params.format}`
}
}
{
keyword: "enum",
params: {
allowedValues: ["admin", "user"]
}
}
Сообщение:
function formatEnum(error) {
return (
"Допустимые значения: " +
error.params.allowedValues.join(", ")
)
}
const schema = {
type: "object",
additionalProperties: false
}
Ошибка:
{
keyword: "additionalProperties",
params: {
additionalProperty: "unknownField"
}
}
Сообщение:
function formatAdditional(error) {
return (
`Поле ${error.params.additionalProperty} запрещено`
)
}
function validateBody(schema) {
const validate = ajv.compile(schema)
return (req, res, next) => {
const valid = validate(req.body)
if (!valid) {
return res.status(400).json({
errors: formatAjvErrors(validate.errors)
})
}
next()
}
}
Использование:
app.post(
"/users",
validateBody(userSchema),
controller
)
class ValidationError extends Error {
constructor(errors) {
super("Validation failed")
this.name = "ValidationError"
this.errors = errors
}
}
Использование:
if (!valid) {
throw new ValidationError(
formatAjvErrors(validate.errors)
)
}
app.use((err, req, res, next) => {
if (err instanceof ValidationError) {
return res.status(400).json({
type: "validation_error",
errors: err.errors
})
}
res.status(500).json({
error: "Internal Server Error"
})
})
function logValidationErrors(errors) {
for (const error of errors) {
console.error({
field: error.instancePath,
keyword: error.keyword,
params: error.params
})
}
}
Стандартные ошибки могут раскрывать:
Плохой пример:
must match pattern "^([A-Z]{3})$"
Безопаснее:
Неверный формат значения
{
email: {
type: "required",
message: "Email обязателен"
}
}
Преобразование:
function toReactHookForm(errors) {
return errors.reduce((acc, error) => {
const field = normalizePath(error.instancePath)
acc[field] = {
type: error.keyword,
message: error.message
}
return acc
}, {})
}
{
"errors": [
{
"code": "ERR_REQUIRED_EMAIL",
"field": "email"
}
]
}
Frontend самостоятельно выбирает текст сообщения по коду ошибки.
Преимущества:
{
type: "string"
}
null вызовет ошибку type.
Кастомное сообщение:
function formatType(error) {
if (error.params.type === "string") {
return "Поле должно быть строкой"
}
return "Неверный тип"
}
function processAjvErrors(errors) {
return errors
.map(normalizeError)
.map(localizeError)
.map(attachErrorCode)
.map(hideInternalDetails)
}
Каждый этап отвечает только за одну задачу:
| Этап | Назначение |
|---|---|
| normalizeError | Нормализация структуры |
| localizeError | Перевод сообщений |
| attachErrorCode | Добавление кодов |
| hideInternalDetails | Сокрытие технических деталей |
Такой подход особенно полезен в: