Default значения

В Joi значения по умолчанию задаются через метод default(), который позволяет автоматически подставлять значение в случае отсутствия поля в входных данных или при его явной неопределённости (undefined). Этот механизм является ключевым инструментом при построении устойчивых схем валидации, так как позволяет не только валидировать данные, но и нормализовать их структуру до единого ожидаемого формата.

Метод default() принимает значение, которое будет использовано, если входное значение отсутствует.

import Joi from 'joi';

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

schema.validate({});
// результат: { role: 'user' }

Если поле присутствует, но содержит undefined, значение по умолчанию также применяется:

schema.validate({ role: undefined });
// результат: { role: 'user' }

Если же поле содержит валидное значение, оно сохраняется без изменений:

schema.validate({ role: 'admin' });
// результат: { role: 'admin' }

Различие между undefined и null

Важное поведение Joi связано с обработкой null. По умолчанию null не заменяется значением default, если явно не указано обратное.

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

schema.validate({ role: null });
// результат: { role: null }

Для того чтобы null также заменялся значением по умолчанию, используется комбинация .default() и .allow(null) с дополнительной логикой:

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

Однако в таком виде null всё равно может пройти как допустимое значение. Для принудительной замены используется предобработка через empty() или кастомная логика.

Функциональные значения по умолчанию

Joi поддерживает использование функций в качестве значения default. Это позволяет вычислять значение динамически при каждой валидации.

const schema = Joi.object({
  createdAt: Joi.date().default(() => new Date(), 'current date')
});

Каждый вызов validate() будет генерировать новое значение даты, а не переиспользовать одно и то же.

Функция может принимать объект контекста:

const schema = Joi.object({
  id: Joi.string(),
  ref: Joi.string().default((value, helpers) => {
    return `ref-${helpers.state.ancestors[0].id}`;
  })
});

Здесь используется доступ к уже валидированным данным через helpers.state.

Контекстные значения и зависимости

Default может зависеть от других полей схемы. Это достигается через ссылки (Joi.ref) или через функции.

const schema = Joi.object({
  currency: Joi.string().default('USD'),
  price: Joi.number(),
  priceWithTax: Joi.number().default((value, helpers) => {
    const { price, currency } = helpers.state.ancestors[0];
    return currency === 'USD' ? price * 1.2 : price;
  })
});

Такой подход позволяет строить производные значения прямо на этапе валидации.

Поведение default и required

Методы default() и required() не конфликтуют напрямую, но влияют на итоговую обязательность значения.

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

Если поле отсутствует, default подставится, и требование required() будет выполнено автоматически.

Однако если значение невозможно вывести (например, default — функция, возвращающая undefined), поведение может привести к ошибке валидации.

Использование default с объектами и массивами

Default может задавать сложные структуры, включая объекты и массивы.

const schema = Joi.object({
  settings: Joi.object({
    theme: Joi.string().default('dark'),
    notifications: Joi.boolean().default(true)
  }).default()
});

Если settings отсутствует, он будет создан целиком с вложенными значениями.

Для массивов:

const schema = Joi.object({
  tags: Joi.array().items(Joi.string()).default([])
});

Важно учитывать, что массив и объект должны создаваться заново при каждой валидации, иначе возможны проблемы с мутацией общего объекта.

Lazy default значения

Для сложных сценариев используется Joi.lazy(), позволяющий откладывать вычисление схемы и значений.

const schema = Joi.object({
  config: Joi.lazy(() =>
    Joi.object({
      version: Joi.number().default(1)
    })
  )
});

Это полезно при рекурсивных структурах или динамической генерации схем.

Приоритет значений при валидации

Joi применяет строгий порядок обработки значений:

  1. Входное значение, если оно определено
  2. Проверка на undefined
  3. Применение default()
  4. Применение преобразований (coercion)
  5. Финальная валидация

Это означает, что default всегда применяется до финальной проверки ограничений типа.

Совместимость default с преобразованиями

Joi может сначала подставить default, а затем преобразовать значение:

const schema = Joi.object({
  count: Joi.number().default('10')
});

schema.validate({});
// результат: { count: 10 }

Строка '10' сначала подставляется, затем приводится к числу.

Отключение default поведения

В некоторых случаях требуется запретить автоматическую подстановку значений. Это делается через .presence('required') на уровне объекта или через явное управление схемой.

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

Здесь отсутствие значения приведёт к ошибке, даже если default мог бы быть задан.

Особенности сериализации результата

После валидации Joi возвращает уже преобразованный объект, где default значения становятся частью результата. Это важно учитывать при дальнейшей передаче данных:

const result = schema.validate({});
console.log(result.value);
// содержит все default-поля

Если используется опция stripUnknown, она не влияет на default, но может удалить лишние входные поля до применения схемы.

Распространённые ошибки при использовании default

Часто встречается проблема повторного использования объектов:

const defaultSettings = {
  theme: 'dark'
};

const schema = Joi.object({
  settings: Joi.object().default(defaultSettings)
});

Такой подход может привести к общей ссылке на объект. Более безопасный вариант:

Joi.object().default(() => ({ theme: 'dark' }))

Это гарантирует создание нового объекта при каждой валидации.

Поведение default в сложных вложенных схемах

Вложенные default применяются рекурсивно, но только в пределах явно определённых схем:

const schema = Joi.object({
  user: Joi.object({
    profile: Joi.object({
      age: Joi.number().default(18)
    })
  })
});

Если user или profile отсутствуют, default для age не сработает, пока не будут определены промежуточные объекты. Для этого требуется задавать default на каждом уровне вложенности.