Структура объекта

Валидация объектов — центральная часть библиотеки Joi. Именно объектные схемы позволяют описывать структуру данных API, конфигураций, DTO, форм, параметров запросов и сложных вложенных сущностей.

Базовая схема объекта создаётся через Joi.object():

const Joi = require('joi');

const schema = Joi.object({
    username: Joi.string(),
    age: Joi.number()
});

Схема описывает:

  • набор допустимых полей;
  • типы значений;
  • обязательность свойств;
  • ограничения;
  • вложенные структуры;
  • связи между полями.

Создание объектной схемы

Минимальный пример

const schema = Joi.object({
    name: Joi.string().required(),
    age: Joi.number().integer()
});

Проверка:

const result = schema.validate({
    name: 'Alex',
    age: 25
});

console.log(result.error);

Обязательные и необязательные поля

required()

const schema = Joi.object({
    login: Joi.string().required(),
    password: Joi.string().required()
});

Если поле отсутствует:

{
    password: '123456'
}

Ошибка:

"login" is required

optional()

Все поля по умолчанию необязательны, но optional() иногда используют для явного указания поведения.

const schema = Joi.object({
    middleName: Joi.string().optional()
});

forbidden()

Поле запрещено.

const schema = Joi.object({
    role: Joi.forbidden()
});

Работа с неизвестными полями

По умолчанию Joi запрещает поля, не описанные в схеме.

Пример ошибки

const schema = Joi.object({
    name: Joi.string()
});

const result = schema.validate({
    name: 'Max',
    city: 'London'
});

Ошибка:

"city" is not allowed

Разрешение дополнительных свойств

unknown(true)

const schema = Joi.object({
    name: Joi.string()
}).unknown(true);

Теперь дополнительные поля разрешены.


unknown(false)

Явный запрет:

const schema = Joi.object({
    name: Joi.string()
}).unknown(false);

Вложенные объекты

Простая вложенность

const schema = Joi.object({
    user: Joi.object({
        name: Joi.string().required(),
        email: Joi.string().email().required()
    }).required()
});

Проверка:

const result = schema.validate({
    user: {
        name: 'John',
        email: 'john@mail.com'
    }
});

Глубокая вложенность

const schema = Joi.object({
    company: Joi.object({
        address: Joi.object({
            city: Joi.string(),
            street: Joi.string(),
            building: Joi.number()
        })
    })
});

Массивы объектов

Очень распространённый сценарий при работе с API.

const schema = Joi.object({
    users: Joi.array().items(
        Joi.object({
            id: Joi.number().required(),
            name: Joi.string().required()
        })
    )
});

Проверка:

const data = {
    users: [
        { id: 1, name: 'Alex' },
        { id: 2, name: 'Kate' }
    ]
};

Значения по умолчанию

default()

const schema = Joi.object({
    role: Joi.string().default('user')
});

Проверка:

const result = schema.validate({});

Результат:

{
    role: 'user'
}

Переименование полей

rename()

const schema = Joi.object({
    name: Joi.string()
}).rename('username', 'name');

Проверка:

const result = schema.validate({
    username: 'Alex'
});

Результат:

{
    name: 'Alex'
}

Изменение структуры результата

strip()

Удаляет поле из результата после успешной проверки.

const schema = Joi.object({
    username: Joi.string(),
    password: Joi.string().strip()
});

Проверка:

const result = schema.validate({
    username: 'admin',
    password: 'secret'
});

Результат:

{
    username: 'admin'
}

Зависимости между полями

Объектные схемы особенно мощны благодаря поддержке логических зависимостей.


with()

Поле требует присутствия другого поля.

const schema = Joi.object({
    password: Joi.string(),
    repeatPassword: Joi.string()
}).with('password', 'repeatPassword');

Если есть password, то должен быть и repeatPassword.


without()

Запрещает совместное использование.

const schema = Joi.object({
    token: Joi.string(),
    password: Joi.string()
}).without('token', 'password');

and()

Все поля должны существовать одновременно.

const schema = Joi.object({
    latitude: Joi.number(),
    longitude: Joi.number()
}).and('latitude', 'longitude');

or()

Хотя бы одно поле обязательно.

const schema = Joi.object({
    phone: Joi.string(),
    email: Joi.string()
}).or('phone', 'email');

xor()

Разрешено только одно поле.

const schema = Joi.object({
    id: Joi.number(),
    uuid: Joi.string()
}).xor('id', 'uuid');

oxor()

Не более одного поля.

const schema = Joi.object({
    email: Joi.string(),
    telegram: Joi.string()
}).oxor('email', 'telegram');

nand()

Запрещённая комбинация.

const schema = Joi.object({
    startDate: Joi.date(),
    archived: Joi.boolean()
}).nand('startDate', 'archived');

assert()

Проверка пользовательских зависимостей.

const schema = Joi.object({
    min: Joi.number(),
    max: Joi.number()
}).assert(
    '.max',
    Joi.number().greater(Joi.ref('min')),
    'max must be greater than min'
);

Ссылки между полями

Joi.ref()

const schema = Joi.object({
    password: Joi.string().required(),
    repeatPassword: Joi.any()
        .valid(Joi.ref('password'))
        .required()
});

Работа с шаблонами ключей

pattern()

Позволяет валидировать динамические ключи.

const schema = Joi.object().pattern(
    /^item_/,
    Joi.number()
);

Допустимо:

{
    item_1: 10,
    item_2: 20
}

Недопустимо:

{
    item_1: 'abc'
}

Ограничение количества полей

min()

Минимальное число ключей.

const schema = Joi.object({
    a: Joi.any(),
    b: Joi.any(),
    c: Joi.any()
}).min(2);

max()

const schema = Joi.object({
    a: Joi.any(),
    b: Joi.any(),
    c: Joi.any()
}).max(2);

length()

Точное количество ключей.

const schema = Joi.object({
    a: Joi.any(),
    b: Joi.any()
}).length(2);

Извлечение схем

extract()

Позволяет получить часть схемы.

const schema = Joi.object({
    profile: Joi.object({
        email: Joi.string().email()
    })
});

const emailSchema = schema.extract('profile.email');

Описание схемы

describe()

Метод возвращает внутреннее представление схемы.

const schema = Joi.object({
    name: Joi.string().required()
});

console.log(schema.describe());

Результат содержит:

  • типы;
  • флаги;
  • правила;
  • ограничения;
  • вложенные структуры.

Частичная модификация схемы

fork()

Позволяет изменить несколько полей.

const schema = Joi.object({
    username: Joi.string(),
    email: Joi.string(),
    password: Joi.string()
});

const updatedSchema = schema.fork(
    ['username', 'email'],
    field => field.required()
);

Изменение присутствия всех полей

prefs()

const schema = Joi.object({
    name: Joi.string(),
    email: Joi.string()
}).prefs({
    presence: 'required'
});

Теперь все поля обязательны.


Разделение схем

Крупные объектные схемы обычно разбивают на отдельные части.

Пример композиции

const addressSchema = Joi.object({
    city: Joi.string(),
    street: Joi.string()
});

const userSchema = Joi.object({
    name: Joi.string(),
    address: addressSchema
});

Повторное использование схем

concat()

const baseSchema = Joi.object({
    id: Joi.number()
});

const extendedSchema = baseSchema.concat(
    Joi.object({
        name: Joi.string()
    })
);

Альтернативные структуры объекта

alternatives()

const schema = Joi.alternatives().try(
    Joi.object({
        type: Joi.string().valid('user'),
        username: Joi.string().required()
    }),

    Joi.object({
        type: Joi.string().valid('admin'),
        permissions: Joi.array().required()
    })
);

Условная структура объекта

when()

const schema = Joi.object({
    type: Joi.string().required(),

    value: Joi.when('type', {
        is: 'email',
        then: Joi.string().email(),
        otherwise: Joi.string().min(3)
    })
});

Валидация объектов API

Типичный пример для REST API:

const createUserSchema = Joi.object({
    username: Joi.string()
        .min(3)
        .max(30)
        .required(),

    email: Joi.string()
        .email()
        .required(),

    password: Joi.string()
        .min(8)
        .required(),

    age: Joi.number()
        .integer()
        .min(18),

    roles: Joi.array().items(
        Joi.string()
    ).default(['user'])
});

Валидация конфигурационных объектов

const configSchema = Joi.object({
    port: Joi.number()
        .port()
        .required(),

    host: Joi.string()
        .hostname()
        .required(),

    database: Joi.object({
        user: Joi.string().required(),
        password: Joi.string().required(),
        dbName: Joi.string().required()
    }).required()
});

Кастомная логика объекта

custom()

const schema = Joi.object({
    start: Joi.date(),
    end: Joi.date()
}).custom((value, helpers) => {

    if (value.start > value.end) {
        return helpers.error('any.invalid');
    }

    return value;
});

Сообщения ошибок

messages()

const schema = Joi.object({
    username: Joi.string().required()
}).messages({
    'any.required': 'Поле обязательно',
    'string.base': 'Должна быть строка'
});

Abort Early

По умолчанию проверка прекращается после первой ошибки.

Получение всех ошибок

const result = schema.validate(data, {
    abortEarly: false
});

Преобразование значений

Joi умеет автоматически преобразовывать значения.

const schema = Joi.object({
    age: Joi.number()
});

Проверка:

schema.validate({
    age: '25'
});

Строка будет преобразована в число.


Отключение преобразований

const result = schema.validate(data, {
    convert: false
});

strict()

Жёсткий режим проверки.

const schema = Joi.object({
    age: Joi.number()
}).strict();

Использование id()

Позволяет именовать схемы.

const schema = Joi.object({
    name: Joi.string()
}).id('UserSchema');

Метаданные

meta()

const schema = Joi.object({
    name: Joi.string()
}).meta({
    className: 'User'
});

Пометки и описания

description()

const schema = Joi.object({
    email: Joi.string()
        .email()
        .description('User email')
});

Теги

tag()

const schema = Joi.object({
    name: Joi.string()
}).tag('api');

Пример сложной объектной схемы

const orderSchema = Joi.object({

    id: Joi.number()
        .integer()
        .required(),

    customer: Joi.object({

        name: Joi.string()
            .min(2)
            .required(),

        email: Joi.string()
            .email()
            .required()

    }).required(),

    products: Joi.array()
        .items(

            Joi.object({

                productId: Joi.number()
                    .required(),

                quantity: Joi.number()
                    .integer()
                    .min(1)
                    .required()

            })

        )
        .min(1)
        .required(),

    status: Joi.string()
        .valid(
            'new',
            'paid',
            'delivered'
        )
        .default('new'),

    createdAt: Joi.date()
        .default(Date.now)

});

Такие схемы используются:

  • в REST API;
  • в GraphQL;
  • в микросервисах;
  • в системах конфигурации;
  • в middleware;
  • в DTO-валидации;
  • в формах;
  • в ORM-слое;
  • в системах сериализации данных.