Кастомизация сообщений через параметры

Библиотека Yup позволяет полностью контролировать тексты ошибок валидации. Сообщения можно задавать:

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

Кастомизация сообщений особенно важна при разработке:

  • форм регистрации;
  • административных панелей;
  • API-валидации;
  • многоязычных интерфейсов;
  • сложных схем с зависимостями.

Базовая кастомизация сообщений

Каждый метод валидации принимает вторым аргументом сообщение об ошибке.

import * as yup from 'yup';

const schema = yup.string().required('Поле обязательно');

Если значение отсутствует:

await schema.validate('');

Yup выбросит:

ValidationError: Поле обязательно

Сообщения в .min(), .max(), .length()

Минимальная длина

const schema = yup
  .string()
  .min(5, 'Минимум 5 символов');

Максимальная длина

const schema = yup
  .string()
  .max(10, 'Максимум 10 символов');

Точная длина

const schema = yup
  .string()
  .length(6, 'Длина должна быть ровно 6 символов');

Интерполяция параметров

Yup поддерживает шаблонные параметры внутри сообщений.

Доступные переменные зависят от метода валидации.

Пример с ${min}

const schema = yup
  .string()
  .min(5, 'Минимум ${min} символов');

Результат:

Минимум 5 символов

Пример с ${max}

const schema = yup
  .string()
  .max(10, 'Максимум ${max} символов');

Использование ${length}

const schema = yup
  .string()
  .length(8, 'Длина должна быть ${length}');

Основные параметры интерполяции

Для строк

Метод Параметр
min ${min}
max ${max}
length ${length}
matches ${regex}

Для чисел

Метод Параметр
min ${min}
max ${max}
lessThan ${less}
moreThan ${more}

Для дат

Метод Параметр
min ${min}
max ${max}

Использование ${path}

${path} содержит имя поля.

const schema = yup.object({
  email: yup
    .string()
    .required('Поле ${path} обязательно')
});

Ошибка:

Поле email обязательно

Использование ${value}

${value} содержит текущее значение.

const schema = yup
  .number()
  .min(18, 'Возраст ${value} слишком мал');

Сообщения как функция

Вместо строки можно использовать функцию.

Это позволяет динамически формировать текст ошибки.

const schema = yup
  .string()
  .min(5, ({ min }) => {
    return `Нужно минимум ${min} символов`;
  });

Аргументы функции сообщения

Функция получает объект с параметрами.

Пример:

({
  value,
  originalValue,
  path,
  spec,
  min,
  max
})

Динамическая генерация сообщений

Разные сообщения для разных условий

const schema = yup
  .number()
  .min(18, ({ value }) => {
    if (value < 10) {
      return 'Слишком маленькое значение';
    }

    return 'Возраст должен быть не менее 18';
  });

Сообщения в .matches()

Метод .matches() используется для проверки регулярных выражений.

const schema = yup
  .string()
  .matches(
    /^[A-Z]+$/,
    'Допустимы только заглавные буквы'
  );

Отключение пустых значений в .matches()

По умолчанию пустая строка тоже валидируется.

const schema = yup
  .string()
  .matches(
    /^[A-Z]+$/,
    {
      message: 'Только заглавные буквы',
      excludeEmptyString: true
    }
  );

Кастомизация .typeError()

.typeError() задаёт сообщение при неправильном типе данных.

const schema = yup
  .number()
  .typeError('Ожидается число');

Пример

await schema.validate('abc');

Ошибка:

Ожидается число

Кастомизация .required()

const schema = yup
  .string()
  .required('Поле не может быть пустым');

Кастомизация .nullable()

.nullable() разрешает null, но сообщения можно комбинировать.

const schema = yup
  .string()
  .nullable()
  .required('Значение обязательно');

Кастомизация .oneOf()

const schema = yup
  .string()
  .oneOf(
    ['admin', 'user'],
    'Недопустимая роль'
  );

Использование ${values}

В .oneOf() доступен параметр ${values}.

const schema = yup
  .string()
  .oneOf(
    ['red', 'blue'],
    'Допустимые значения: ${values}'
  );

Ошибка:

Допустимые значения: red, blue

Кастомизация .notOneOf()

const schema = yup
  .string()
  .notOneOf(
    ['root'],
    'Значение root запрещено'
  );

Сообщения в .email()

const schema = yup
  .string()
  .email('Некорректный email');

Сообщения в .url()

const schema = yup
  .string()
  .url('Некорректный URL');

Сообщения в .uuid()

const schema = yup
  .string()
  .uuid('Некорректный UUID');

Сообщения в .positive() и .negative()

Положительное число

const schema = yup
  .number()
  .positive('Число должно быть положительным');

Отрицательное число

const schema = yup
  .number()
  .negative('Число должно быть отрицательным');

Сообщения в .integer()

const schema = yup
  .number()
  .integer('Допустимы только целые числа');

Кастомизация сообщений в .test()

.test() предоставляет максимальную гибкость.

Простой пример

const schema = yup.string().test(
  'starts-with-a',
  'Строка должна начинаться с A',
  value => {
    return value.startsWith('A');
  }
);

Динамические сообщения внутри .test()

Можно использовать createError.

const schema = yup.string().test(
  'custom-test',
  'Ошибка',
  function (value) {
    if (!value.includes('@')) {
      return this.createError({
        message: 'Отсутствует символ @'
      });
    }

    return true;
  }
);

Передача параметров в createError

const schema = yup.string().test(
  'username',
  'Ошибка',
  function (value) {
    return this.createError({
      message: '${path}: неверное значение',
      params: {
        path: 'username'
      }
    });
  }
);

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

label() заменяет имя поля в сообщениях.

const schema = yup.object({
  email: yup
    .string()
    .label('Email пользователя')
    .required('${label} обязателен')
});

Ошибка:

Email пользователя обязателен

Глобальная локализация через setLocale

Yup поддерживает глобальную настройку сообщений.

import { setLocale } from 'yup';

setLocale({
  mixed: {
    required: 'Поле обязательно'
  }
});

Теперь все .required() используют это сообщение.


Локализация строковых правил

setLocale({
  string: {
    min: 'Минимум ${min} символов',
    max: 'Максимум ${max} символов'
  }
});

Локализация чисел

setLocale({
  number: {
    min: 'Минимальное значение: ${min}',
    max: 'Максимальное значение: ${max}'
  }
});

Полная структура setLocale

setLocale({
  mixed: {},
  string: {},
  number: {},
  date: {},
  object: {},
  array: {},
  boolean: {}
});

Кастомизация массива ошибок

При abortEarly: false Yup собирает все ошибки.

try {
  await schema.validate(data, {
    abortEarly: false
  });
} catch (error) {
  console.log(error.errors);
}

Пример массива сообщений

[
  'Поле email обязательно',
  'Пароль слишком короткий'
]

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

error.inner содержит подробные объекты ошибок.

catch (error) {
  error.inner.forEach(err => {
    console.log(err.path);
    console.log(err.message);
  });
}

Кастомизация ошибок вложенных объектов

const schema = yup.object({
  profile: yup.object({
    name: yup
      .string()
      .required('Имя обязательно')
  })
});

Сообщения для массивов

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

const schema = yup
  .array()
  .min(2, 'Минимум ${min} элемента');

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

const schema = yup
  .array()
  .max(5, 'Максимум ${max} элементов');

Кастомизация .when()

Условная валидация может использовать разные сообщения.

const schema = yup.object({
  isAdmin: yup.boolean(),

  password: yup.string().when('isAdmin', {
    is: true,

    then: schema =>
      schema.required(
        'Пароль администратора обязателен'
      ),

    otherwise: schema =>
      schema.required(
        'Пароль пользователя обязателен'
      )
  })
});

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

Контекст передаётся через validate.

await schema.validate(data, {
  context: {
    minAge: 21
  }
});

Сообщения с использованием контекста

const schema = yup.number().test(
  'min-age',
  'Ошибка',
  function (value) {
    const { minAge } = this.options.context;

    if (value < minAge) {
      return this.createError({
        message: `Возраст должен быть не менее ${minAge}`
      });
    }

    return true;
  }
);

Формирование человекочитаемых сообщений

Плохой вариант

'field_invalid_value'

Хороший вариант

'Введите корректный email'

Единый стиль сообщений

Желательно придерживаться одного формата:

  • все сообщения в повелительной форме;
  • либо все в описательной форме.

Повелительная форма

'Введите email'
'Укажите пароль'

Описательная форма

'Email обязателен'
'Пароль слишком короткий'

Использование словарей сообщений

Крупные проекты часто выносят сообщения отдельно.

export const messages = {
  required: 'Поле обязательно',
  email: 'Некорректный email',
  minPassword: 'Минимум 8 символов'
};

Применение словаря

const schema = yup.object({
  email: yup
    .string()
    .email(messages.email)
    .required(messages.required)
});

Генераторы сообщений

Иногда удобнее использовать функции.

const messages = {
  min: value => `Минимум ${value} символов`
};

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

const schema = yup
  .string()
  .min(5, messages.min(5));

Централизованная система сообщений

Пример структуры:

src/
  validation/
    messages.js
    locale.js
    schemas/

Типичные ошибки при кастомизации сообщений

Жёстко закодированные строки

Плохо:

.min(5, 'Минимум 5 символов')

Лучше:

.min(5, messages.min(5))

Отсутствие локализации

Плохо:

.required('Required')

Лучше:

.required(t('validation.required'))

Смешивание разных стилей

Плохо:

'Введите email'
'Пароль обязателен'
'Минимум символов: 5'

Интеграция с i18n

Пример с i18next

import i18next from 'i18next';

const schema = yup.string().required(
  i18next.t('validation.required')
);

Динамическая локализация через функции

setLocale({
  mixed: {
    required: () => 'Поле обязательно'
  }
});

Возврат объектов вместо строк

Иногда удобно возвращать структуру для i18n.

setLocale({
  mixed: {
    required: () => ({
      key: 'field_required'
    })
  }
});

Пример обработки

catch (error) {
  error.errors.forEach(err => {
    console.log(err.key);
  });
}

Комбинирование нескольких параметров

const schema = yup
  .string()
  .min(
    5,
    '${path}: минимум ${min} символов'
  );

Использование HTML в сообщениях

При интеграции с UI-фреймворками сообщения иногда содержат HTML.

.required('<b>Поле обязательно</b>')

Такой подход требует осторожности из-за XSS.


Безопасный вариант

Лучше хранить чистый текст:

.required('Поле обязательно')

А оформление выполнять на уровне интерфейса.


Форматирование сложных сообщений

Многострочные сообщения

const message = `
Пароль должен содержать:
- цифру
- спецсимвол
- заглавную букву
`;

Использование шаблонных строк

const min = 8;

const message = `
Минимальная длина пароля: ${min}
`;

Кастомизация сообщений для API

Формат ошибок часто стандартизируют.

{
  field: 'email',
  message: 'Некорректный email'
}

Преобразование ошибок Yup

catch (error) {
  const formatted = error.inner.map(err => ({
    field: err.path,
    message: err.message
  }));

  console.log(formatted);
}

Кастомные коды ошибок

return this.createError({
  message: 'Некорректный email',
  params: {
    code: 'INVALID_EMAIL'
  }
});

Извлечение кодов ошибок

catch (error) {
  error.inner.forEach(err => {
    console.log(err.params.code);
  });
}

Практическая схема с кастомными сообщениями

const schema = yup.object({
  username: yup
    .string()
    .required('Введите имя пользователя')
    .min(3, 'Минимум 3 символа'),

  email: yup
    .string()
    .required('Введите email')
    .email('Некорректный email'),

  password: yup
    .string()
    .required('Введите пароль')
    .min(
      8,
      'Пароль должен содержать минимум 8 символов'
    )
});

Пример результата валидации

[
  'Введите email',
  'Пароль должен содержать минимум 8 символов'
]

Архитектура масштабируемой системы сообщений

Базовый слой

messages/common.js

Содержит:

  • обязательные поля;
  • минимальные длины;
  • стандартные ошибки.

Предметный слой

messages/auth.js
messages/profile.js
messages/payment.js

Содержит сообщения конкретного домена.


Переиспользование сообщений

export const authMessages = {
  invalidPassword:
    'Неверный логин или пароль'
};

Использование в нескольких схемах

.required(authMessages.invalidPassword)

Рекомендации по качеству сообщений

Сообщение должно:

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

Нежелательный вариант

Validation failed at path email

Хороший вариант

Введите корректный email