Опирается на правила:
R-VLD-MSG-1…R-VLD-MSG-3иR-VLD-MSG-X1…R-VLD-MSG-X3из Validation Style Guide → раздел 8. Сообщения и i18n.
Важно знать
message— на русском. Это текст, который попадёт вviolations[].messageи дойдёт до пользователя на UI.- Для пользователя, не для разработчика. «Сумма должна быть положительной», не «
amountmust be positive».- Интерполяция через
{}из стандартных placeholder-ов:{value},{min},{max},{regexp}. Spring подставит реальные значения в момент валидации.- i18n — через
{key}+messages_ru.properties/messages_en.properties. Используется только когда сервис реально мультиязычный.- Default message на custom constraint имеет смысл. Переопределение на каждом поле — только если у конкретного поля свой текст.
- Английский в message — антипаттерн. Этот текст не «техническая ошибка для логов», это «понятное сообщение для пользователя».
- Технические термины в message (
field amount,BigDecimal,null) — антипаттерн. Пользователь не знает, что такоеfield.
message в Jakarta-аннотации — единственный текст, который при невалидном вводе доходит до конечного пользователя через violations[].message в ProblemDetails. Это product copy, не «лог», поэтому правила формирования другие. Раскрытие раздела 8 гайда.
Default — на русском, для пользователя
R-VLD-MSG-1: текст пишется на языке UI (в проекте — русский).
public record CreateCustomerRequest(
@NotBlank(message = "Имя обязательно")
@Size(max = 100, message = "Имя не более 100 символов")
String firstName,
@NotBlank(message = "Телефон обязателен")
@RussianPhone
String phone,
@NotNull(message = "Дата рождения обязательна")
@Past(message = "Дата рождения должна быть в прошлом")
LocalDate birthDate
) {}
Что попадёт в ответ при пустом firstName:
{
"type": "/errors/validation-error",
"title": "Validation failed",
"status": 400,
"violations": [
{"field": "firstName", "message": "Имя обязательно"}
]
}
Фронт показывает «Имя обязательно» рядом с полем. Без перевода на стороне фронта, без догадок.
R-VLD-MSG-X1: английский в message — антипаттерн.
// ПЛОХО
@NotBlank(message = "First name is required")
@Size(max = 100, message = "First name must be at most 100 characters")
String firstName
Что произойдёт:
- Текст попадёт в UI как есть.
- Пользователь увидит «First name is required» и подумает «глюк, форма на английском».
- Перевод на стороне фронта потребует mapping
"First name is required"→ «Имя обязательно». Маппинг постоянно отстаёт от backend.
Backend — единственное место правды для текста ошибок. Перевод на UI стороне — антипаттерн, если backend уже отдаёт нужный язык.
Интерполяция через {} placeholders
R-VLD-MSG-2: динамические значения подставляются из стандартных placeholder-ов.
public record OrderItemRequest(
@NotNull(message = "Артикул обязателен") String sku,
@Min(value = 1, message = "Количество должно быть не меньше {value}")
@Max(value = 9999, message = "Количество не более {value}")
int quantity,
@Size(min = 1, max = 50, message = "Длина имени от {min} до {max} символов")
String displayName,
@Pattern(regexp = "^[A-Z]{3}-\\d{6}$", message = "Артикул в формате {regexp}")
String articleNumber
) {}
Стандартные placeholder-ы по типу аннотации:
| Аннотация | Доступные placeholders |
|---|---|
@Min, @Max | {value} |
@DecimalMin, @DecimalMax | {value}, {inclusive} |
@Size | {min}, {max} |
@Pattern | {regexp}, {flags} |
@Past, @Future | — |
@Email | {regexp}, {flags} |
Что подставляется в ответ при quantity = 0:
{"field": "items[0].quantity", "message": "Количество должно быть не меньше 1"}
Pattern-сам по себе в сообщении — спорная практика. Regex в UI пользователю не помогает. Лучше — human-readable описание:
@Pattern(regexp = "^[A-Z]{3}-\\d{6}$",
message = "Артикул в формате AAA-000000 (три буквы, дефис, шесть цифр)")
Default message на custom constraint
R-VLD-MSG-3: для custom constraint текст пишется один раз в самой аннотации.
// common/validation/RussianPhone.java
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RussianPhoneValidator.class)
public @interface RussianPhone {
String message() default "Номер должен быть в формате +7XXXXXXXXXX";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Использование:
public record CreateCustomerRequest(
@NotBlank(message = "Телефон обязателен") @RussianPhone String phone // default message от @RussianPhone
) {}
Если для конкретного поля нужен другой текст (например в админ-форме «Телефон менеджера» — в default-сообщении это не отражено), переопределяем:
@RussianPhone(message = "Телефон менеджера в формате +7XXXXXXXXXX")
String managerPhone
R-VLD-MSG-X3: переопределение default message без бизнес-причины — антипаттерн.
// ПЛОХО — переопределение без смысла
@RussianPhone(message = "Номер должен быть в формате +7XXXXXXXXXX") // совпадает с default
String phone
@RussianPhone(message = "Номер должен быть в формате +7XXXXXXXXXX") // и снова
String alternatePhone
Если текст совпадает с default — не переопределяем. Это лишний шум, который ещё и нужно синхронизировать с самой аннотацией при смене default.
Технические термины — не для пользователя
R-VLD-MSG-X2: пользователь не знает, что такое field, null, BigDecimal, «property amount must be positive».
// ПЛОХО
@DecimalMin(value = "0.01", message = "Field amount must be positive")
@Pattern(regexp = "...", message = "Property orderNumber doesn't match pattern")
// ХОРОШО
@DecimalMin(value = "0.01", message = "Сумма должна быть положительной")
@Pattern(regexp = "...", message = "Номер заказа в формате AAA-000000")
Что избегаем:
field,property,valueв человеческом тексте.- Java-термины —
null,BigDecimal,String,Integer. - Технический жаргон — «doesn't match pattern», «must satisfy constraint», «violates regex».
- Сообщения от Jakarta default (
must not be null,must not be empty) — заменяем на русские «обязательно», «не должно быть пустым».
Тон сообщения — спокойный, описывающий правило, без обвинения пользователя:
- ✓ «Сумма должна быть положительной»
- ✗ «Вы ввели неправильную сумму»
- ✗ «Ошибка в поле сумма»
i18n через {key} + messages.properties
Если сервис мультиязычный (UI на русском, английском, узбекском), используем bundle-based подход.
public record CreateOrderRequest(
@NotBlank(message = "{order.name.required}") String name,
@DecimalMin(value = "0.01", message = "{order.amount.positive}") BigDecimal amount
) {}
src/main/resources/messages_ru.properties:
order.name.required=Имя заказа обязательно
order.amount.positive=Сумма должна быть положительной
src/main/resources/messages_en.properties:
order.name.required=Order name is required
order.amount.positive=Amount must be positive
Spring выбирает bundle по Accept-Language header клиента (если настроен MessageSource с LocaleResolver).
В UCP-проекте сайт только на русском — bundle-based подход не нужен, сразу пишем русский текст в message(). Локализация — overengineering, если нет реального второго языка.
R-LOC-3 в REST API style guide описывает, как message локализуется на уровне REST-ответа. См. REST API → Локализация.
Полный пример
public record CreateProductRequest(
@NotBlank(message = "Название обязательно")
@Size(min = 1, max = 200, message = "Название от {min} до {max} символов")
String name,
@NotBlank(message = "Артикул обязателен")
@Pattern(regexp = "^[A-Z]{3}-\\d{6}$",
message = "Артикул в формате AAA-000000 (три буквы, дефис, шесть цифр)")
String sku,
@NotNull(message = "Цена обязательна")
@DecimalMin(value = "0.01", message = "Цена должна быть не меньше {value}")
@DecimalMax(value = "999999.99", message = "Цена не более {value}")
BigDecimal price,
@NotNull(message = "Категория обязательна")
@Valid
CategoryRef category,
@Size(max = 5000, message = "Описание не более {max} символов")
String description
) {}
Каждое сообщение — короткое, понятное, на русском, с подставленными значениями где есть смысл.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Английский в message ("must be positive") | R-VLD-MSG-X1 | Русский: «должно быть положительным» |
Технические термины (field, null, BigDecimal) | R-VLD-MSG-X2 | Пользовательский язык («сумма», «дата») |
| Дублирование default message на каждом поле без причины | R-VLD-MSG-X3 | Default — в аннотации; переопределяем только для специфичных полей |
Default Jakarta-сообщения (must not be null) | R-VLD-MSG-1 | Явное русское message («обязательно») |
Hardcoded значения в тексте вместо {value} | R-VLD-MSG-2 | "не меньше {value}" — подставится из аннотации |
Куда дальше
- Validation → раздел 8. Сообщения и i18n — нормативные формулировки
R-VLD-MSG-*. - Стандартные constraints — какие placeholder-ы доступны на каких аннотациях.
- Custom constraints — где задаётся default message для custom-constraints.
- REST API → Локализация —
R-LOC-*про язык ответов. - Error Handling → ProblemDetails mapping — как
violationsвстраиваются в общий ProblemDetails.