Паттерны ключей

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

Механизм особенно полезен в случаях:

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

Основной инструмент — метод object.pattern().


Метод object.pattern()

Базовый синтаксис:

Joi.object().pattern(pattern, schema)

Аргументы:

Аргумент Назначение
pattern Регулярное выражение или схема Joi
schema Правила валидации значений

Простая проверка динамических ключей

Пример объекта с неизвестными заранее ключами:

const Joi = require('joi');

const schema = Joi.object().pattern(
  /^user_/,
  Joi.string()
);

const data = {
  user_name: 'Alex',
  user_city: 'London'
};

const result = schema.validate(data);

console.log(result.error);

Правило означает:

  • каждый ключ, начинающийся с user_,
  • должен содержать строковое значение.

Несоответствие значения схеме

const data = {
  user_name: 'Alex',
  user_age: 25
};

Ошибка:

"user_age" must be a string

Хотя ключ соответствует паттерну, значение нарушает правило Joi.string().


Проверка всех ключей объекта

Регулярное выражение /.*/ позволяет применять правило ко всем свойствам.

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

Допустимые данные:

{
  width: 100,
  height: 200,
  depth: 50
}

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

{
  width: 100,
  title: 'Box'
}

Ограничение формата ключей

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

Только числовые ключи

const schema = Joi.object().pattern(
  /^[0-9]+$/,
  Joi.string()
);

Допустимо:

{
  1: 'one',
  2: 'two'
}

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

{
  abc: 'text'
}

UUID-ключи

const schema = Joi.object().pattern(
  /^[0-9a-fA-F-]{36}$/,
  Joi.boolean()
);

Пример:

{
  '550e8400-e29b-41d4-a716-446655440000': true
}

Паттерны для языковых переводов

const schema = Joi.object().pattern(
  /^[a-z]{2}$/,
  Joi.string()
);

Подходящий объект:

{
  en: 'Hello',
  fr: 'Bonjour',
  de: 'Hallo'
}

Использование сложных схем значений

Паттерн может описывать не только примитивы.

const schema = Joi.object().pattern(
  /^product_/,
  Joi.object({
    title: Joi.string().required(),
    price: Joi.number().positive().required()
  })
);

Данные:

{
  product_1: {
    title: 'Phone',
    price: 500
  },
  product_2: {
    title: 'Laptop',
    price: 1500
  }
}

Комбинация keys() и pattern()

Часто часть полей известна заранее, а часть — динамическая.

const schema = Joi.object({
  id: Joi.number().required(),
  createdAt: Joi.date()
}).pattern(
  /^meta_/,
  Joi.string()
);

Допустимые данные:

{
  id: 1,
  createdAt: '2025-01-01',
  meta_author: 'Admin',
  meta_source: 'API'
}

Запрет неизвестных ключей

По умолчанию объект может содержать дополнительные свойства.

Для полного контроля используется .unknown(false).

const schema = Joi.object({
  id: Joi.number()
})
.pattern(/^meta_/, Joi.string())
.unknown(false);

Допустимо:

{
  id: 1,
  meta_author: 'Admin'
}

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

{
  id: 1,
  random: true
}

Ошибка:

"random" is not allowed

Несколько паттернов

В объекте можно определять несколько правил.

const schema = Joi.object()
  .pattern(/^str_/, Joi.string())
  .pattern(/^num_/, Joi.number())
  .pattern(/^bool_/, Joi.boolean());

Пример:

{
  str_name: 'Alex',
  num_age: 30,
  bool_admin: true
}

Пересечение паттернов

Если ключ соответствует нескольким паттернам, Joi применяет все правила.

const schema = Joi.object()
  .pattern(/^item_/, Joi.object())
  .pattern(/_admin$/, Joi.boolean());

Проблемный пример:

{
  item_admin: true
}

Ключ:

  • соответствует ^item_
  • соответствует _admin$

Следовательно:

  • значение должно быть объектом;
  • значение должно быть boolean.

Это создаёт конфликт.


Использование matches

Метод matches позволяет валидировать количество совпадений.

Минимальное число совпадающих ключей

const schema = Joi.object().pattern(
  /^feature_/,
  Joi.boolean(),
  {
    matches: Joi.array().min(2)
  }
);

Требуется минимум два ключа:

{
  feature_chat: true,
  feature_upload: false
}

Ограничение максимального количества

const schema = Joi.object().pattern(
  /^tag_/,
  Joi.string(),
  {
    matches: Joi.array().max(3)
  }
);

Проверка точного количества

const schema = Joi.object().pattern(
  /^phone_/,
  Joi.string(),
  {
    matches: Joi.array().length(2)
  }
);

Объект обязан содержать ровно два ключа phone_*.


Использование схемы вместо регулярного выражения

Вместо RegExp допускается схема Joi.

const schema = Joi.object().pattern(
  Joi.string().min(3),
  Joi.number()
);

Правило:

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

Вложенные паттерны

Паттерны могут использоваться рекурсивно.

const schema = Joi.object({
  settings: Joi.object().pattern(
    /^module_/,
    Joi.object({
      enabled: Joi.boolean(),
      version: Joi.string()
    })
  )
});

Данные:

{
  settings: {
    module_auth: {
      enabled: true,
      version: '1.0'
    },
    module_payments: {
      enabled: false,
      version: '2.1'
    }
  }
}

Валидация ENV-переменных

Один из наиболее распространённых сценариев.

const schema = Joi.object({
  NODE_ENV: Joi.string()
})
.pattern(
  /^APP_/,
  Joi.string().required()
);

Подходящий объект:

{
  NODE_ENV: 'production',
  APP_PORT: '3000',
  APP_SECRET: 'token'
}

Карта настроек

const schema = Joi.object().pattern(
  /^[a-z0-9_.-]+$/,
  Joi.alternatives().try(
    Joi.string(),
    Joi.number(),
    Joi.boolean()
  )
);

Допустимые ключи:

{
  'ui.theme': 'dark',
  'server.port': 8080,
  'cache.enabled': true
}

Ограничение типов динамических значений

Только массивы

const schema = Joi.object().pattern(
  /^list_/,
  Joi.array().items(Joi.string())
);

Только даты

const schema = Joi.object().pattern(
  /^date_/,
  Joi.date()
);

Только положительные числа

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

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

Паттерны хорошо сочетаются с альтернативными схемами.

const schema = Joi.object().pattern(
  /^config_/,
  Joi.alternatives().try(
    Joi.string(),
    Joi.number(),
    Joi.boolean()
  )
);

Генерация ошибок

Пример ошибки ключа

const schema = Joi.object().pattern(
  /^user_/,
  Joi.string()
);

const result = schema.validate({
  admin: true
});

Если unknown(false) не используется, ошибка не возникнет, потому что правило применяется только к совпадающим ключам.


Совместное использование с unknown(false)

const schema = Joi.object()
  .pattern(/^user_/, Joi.string())
  .unknown(false);

Теперь:

{
  admin: true
}

вызовет:

"admin" is not allowed

Приоритет обычных ключей

Явно определённые поля имеют более высокий приоритет.

const schema = Joi.object({
  user_id: Joi.number()
}).pattern(
  /^user_/,
  Joi.string()
);

Поле:

{
  user_id: 5
}

валидно, несмотря на паттерн Joi.string().


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

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

const schema = Joi.object().pattern(
  /^prop_/,
  Joi.string()
);

Такой объект валиден:

{}

Для обязательного наличия используются matches.

const schema = Joi.object().pattern(
  /^prop_/,
  Joi.string(),
  {
    matches: Joi.array().min(1)
  }
);

Ограничение длины ключа

const schema = Joi.object().pattern(
  /^[a-z]{3,10}$/,
  Joi.number()
);

Паттерны с регистрами

Только верхний регистр

const schema = Joi.object().pattern(
  /^[A-Z_]+$/,
  Joi.string()
);

Только нижний регистр

const schema = Joi.object().pattern(
  /^[a-z_]+$/,
  Joi.string()
);

Валидация объектных индексов

const schema = Joi.object().pattern(
  /^item_[0-9]+$/,
  Joi.object({
    value: Joi.string().required()
  })
);

Пример:

{
  item_1: { value: 'A' },
  item_2: { value: 'B' }
}

Проверка вложенных словарей

const schema = Joi.object({
  translations: Joi.object().pattern(
    /^[a-z]{2}$/,
    Joi.object({
      title: Joi.string(),
      description: Joi.string()
    })
  )
});

Частые ошибки

Слишком общий паттерн

/.*/

Такое выражение перехватывает все ключи и может конфликтовать с другими правилами.


Отсутствие unknown(false)

Паттерн не запрещает остальные свойства автоматически.

Joi.object()
  .pattern(/^cfg_/, Joi.string());

Объект:

{
  random: true
}

будет валиден.


Конфликт паттернов

.pattern(/^a/, Joi.string())
.pattern(/z$/, Joi.number())

Ключ:

abz

должен одновременно быть:

  • строкой;
  • числом.

Практический пример конфигурации

const schema = Joi.object({
  appName: Joi.string().required(),
  port: Joi.number().required()
})
.pattern(
  /^feature_/,
  Joi.boolean()
)
.pattern(
  /^env_/,
  Joi.string()
)
.unknown(false);

Допустимые данные:

{
  appName: 'MyApp',
  port: 3000,

  feature_auth: true,
  feature_cache: false,

  env_mode: 'production',
  env_region: 'eu'
}

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

{
  appName: 'MyApp',
  port: 3000,
  random: 123
}

Ошибка:

"random" is not allowed