Опирается на правила:
R-VLD-STD-1…R-VLD-STD-5иR-VLD-STD-X1…R-VLD-STD-X3из Validation Style Guide → раздел 2. Стандартные constraints.
Важно знать
@NotNullvs@NotBlankvs@NotEmpty— три разные аннотации для трёх типов: object/Boolean, String, коллекция. Не взаимозаменяемы.@NotBlankдля строки = «не null, не пустая, не из одних пробелов». Одна аннотация вместо тройки.- Числовые:
@Min/@Maxдля примитивов и боксированных,@DecimalMin/@DecimalMaxдляBigDecimal/BigInteger.@MinнаBigDecimal— антипаттерн.@Pattern— для редких доменных форматов (артикул[A-Z]{3}-\d{6}). Для частых (телефон, ИНН, BIC) — custom constraint.@Past/@FutureдляLocalDate/Instant/OffsetDateTime— встроенные, инвариантны к таймзоне сервера.@NotNullна примитиве (@NotNull int amount) — молчаливо ничего не делает. Примитив не может быть null. Ложная гарантия.
Jakarta Validation покрывает 80% типичных валидаций входных DTO. Стандартные аннотации — это контракт, который читается без объяснений: разработчик, ревьюер и codegen из OpenAPI одинаково их понимают. Свой @ValidAmount-velocipede ломает эту общую базу. Раскрытие раздела 2 гайда.
@NotNull vs @NotBlank vs @NotEmpty
R-VLD-STD-1: три аннотации для трёх категорий, не взаимозаменяемы.
public record CreateCustomerRequest(
@NotNull CustomerId externalId, // ← object, Boolean
@NotBlank @Size(max = 100) String firstName, // ← String
@NotEmpty @Valid List<AddressRequest> addresses // ← Collection / Array / Map
) {}
Различия:
| Аннотация | Применяется к | Что проверяет |
|---|---|---|
@NotNull | object-поля, Boolean, любой тип-обёртка | value != null |
@NotBlank | String | value != null && !value.trim().isEmpty() |
@NotEmpty | String, Collection, Map, array | value != null && size() > 0 |
Частая ошибка — @NotNull String name: проверит, что строка не null, но пропустит "" и " ". Если бизнес-смысл — «непустое имя», нужен @NotBlank.
Ещё одна — @NotEmpty String name: формально работает (name != null && name.length() > 0), но допускает " ". Для строк — почти всегда @NotBlank.
@NotEmpty уместен для коллекций (где «пусто» = isEmpty()), а не для строк.
Размеры — @Size, @Min/@Max, @DecimalMin/@DecimalMax
R-VLD-STD-2: правильная аннотация по типу.
public record OrderItemRequest(
@NotBlank @Size(max = 64) String sku,
@Min(1) @Max(9999) int quantity,
@NotNull @DecimalMin("0.01") @DecimalMax("999999.99") BigDecimal unitPrice
) {}
Правила:
@Size(min, max)— длина строки или размер коллекции. Не для чисел.@Min/@Max— для целочисленных примитивов и обёрток (int,Integer,long,Long,short,byte).@DecimalMin/@DecimalMax— дляBigDecimal/BigInteger. ПринимаютString("0.01") — потому что в аннотацию нельзя положить BigDecimal-литерал.@Positive,@PositiveOrZero,@Negative,@NegativeOrZero— короткая форма для знака. Эквивалентны@Min(1),@Min(0),@Max(-1),@Max(0).
@Min на BigDecimal — типичная ошибка:
// ПЛОХО — @Min не работает на BigDecimal (через Number-конверсию silent-passes)
@Min(1) BigDecimal amount
// ХОРОШО
@DecimalMin("1") BigDecimal amount
@Min формально принимает любой Number через адаптер, но компилируется и не ругается, а валидация на больших числах работает неправильно из-за конверсии double. @DecimalMin использует BigDecimal-арифметику.
Формат — @Email, @Pattern узко
R-VLD-STD-3: для частых форматов есть готовые аннотации.
public record CustomerProfile(
@NotBlank @Email @Size(max = 254) String email,
@NotBlank @Pattern(regexp = "^[A-Z]{3}-\\d{6}$") String orderNumber, // редкий доменный формат
@NotBlank @RussianPhone String phone // custom для частого формата
) {}
@Email:
- Использует встроенную проверку Hibernate Validator по RFC 5322/5321.
- Обновляется вместе с библиотекой — когда меняются стандарты, апгрейд автоматический.
- Не пропускает банальные ошибки типа
user@илиuser @example.com.
Кастомный regex для email — R-VLD-STD-X2, антипаттерн:
// ПЛОХО — велосипед, валидирует хуже
@Pattern(regexp = "^[^@]+@[^@]+$") String email
// ХОРОШО
@Email String email
Кастомный regex принимает user@@example, @example.com, user@. Дорабатывать регулярку до правильного RFC — не задача, которую кто-то должен решать снова и снова. Используем стандарт.
@Pattern остаётся для редких доменных форматов:
- Артикул товара
^[A-Z]{3}-\d{6}$. - Внутренний ID контракта
^CTR-\d{4}-\d{8}$. - VIN-номер автомобиля.
Если формат частый и встречается на 3+ полях в проекте (телефон, ИНН, ОГРН, BIC, IBAN) — это сигнал к custom constraint (см. Custom constraints). Тогда правило именуется (@RussianPhone, @VatNumber) и переиспользуется без копипасты regex.
Время — @Past, @Future, @PastOrPresent, @FutureOrPresent
R-VLD-STD-4: для LocalDate, LocalDateTime, Instant, OffsetDateTime, ZonedDateTime.
public record CreateContractRequest(
@NotNull @FutureOrPresent LocalDate validFrom, // не в прошлом
@NotNull @Future LocalDate validUntil, // только в будущем
@NotNull @PastOrPresent Instant signedAt // не в будущем
) {}
Что хорошо:
- Не нужно сравнивать с
Instant.now()руками. - Учитывает clock-source через
Clockбин (полезно в тестах с фиксированным временем — см. Test Strategy). - Семантическое имя — читается как требование.
Замечание про cross-field: правило «validFrom ≤ validUntil» — это уже cross-field, для него @DateRange class-level constraint (см. Cross-field validation). @FutureOrPresent каждое поле проверяет в одиночку.
Тип-зависимая валидация
R-VLD-STD-5: выбор типа сам по себе несёт смысл.
Boolean, который может быть null:
@NotNull Boolean acceptedTerms // явное согласие — должно быть true/false, не оставлено пустым
vs
boolean acceptedTerms // примитив, по дефолту false — но это **не** «согласился ложно»
Если поле обязательно и должно быть явным (явное согласие, явный выбор true/false), используем Boolean + @NotNull. Если default — допустимый ответ, примитив.
Целое, которое не может быть пустым:
@Min(1) int quantity // примитив, всегда есть значение; @Min проверит положительность
vs
@NotNull @Min(1) Integer quantity // обёртка — нужен @NotNull, иначе допустит null
Если 0 — невалидное значение и не должно проскочить, используем примитив + @Min(1). Если null допустим — обёртка + @NotNull (если обязательно) или ничего (если optional).
@NotNull на примитиве — антипаттерн
R-VLD-STD-X1: @NotNull int amount — компилируется, не падает, молча ничего не проверяет.
// ПЛОХО — ложная гарантия
public record OrderItemRequest(
@NotNull int quantity // ← примитив не может быть null, аннотация бесполезна
) {}
Что не так:
- Ложная безопасность. Разработчик думает «защищён от null» — но
intуже защищён, аннотация ничего не добавляет. - Code review проходит. Аннотация выглядит правильно, ревьюер не замечает.
- Скрытое значение по дефолту. Если в JSON приходит
null, Jackson превращает его в0(Java-дефолт дляint).@NotNullего не ловит. На выходе — order с quantity=0, что бизнес-инвариант нарушает.
Правильно:
// Вариант А — если 0 невалидный
@Min(1) int quantity
// Вариант Б — если поле обязательно и нужно отличать «не пришло» от «0»
@NotNull @Min(1) Integer quantity
В варианте Б Integer (обёртка) принимает null от Jackson, и @NotNull ловит. С int это невозможно.
Composite-аннотации проекта — не плодим
R-VLD-STD-X3: соблазн сделать @NotBlankAndAtMost50 и применять одну вместо двух. Не делаем.
// ПЛОХО — composite на стандартных
@NotBlankAndAtMost50 String firstName
vs
// ХОРОШО — две явных
@NotBlank @Size(max = 50) String firstName
Почему две явных лучше:
- Читается без походов в код.
@NotBlank @Size(max = 50)сразу говорит о двух правилах.@NotBlankAndAtMost50— нужно открыть аннотацию. - Изменения локальны. Захотел поменять
50на100— меняешь только@Size. С composite — либо новый@NotBlankAndAtMost100, либо переименование (ломаем все вызовы). - Codegen из OpenAPI генерирует две. Composite в проекте появляется только руками — не сочетается с auto-generated DTO.
Composite-аннотации уместны только для доменных правил, где имя несёт бизнес-смысл (например @VatNumber = @NotBlank @Size(min=10, max=12) @Pattern(...)). Для технических комбинаций — не нужны.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
@NotNull на примитиве (@NotNull int amount) | R-VLD-STD-X1 | @Min(1) int либо @NotNull @Min(1) Integer |
Кастомный regex для email вместо @Email | R-VLD-STD-X2 | @Email |
Composite-аннотация (@NotBlankAndAtMost50) поверх стандартных | R-VLD-STD-X3 | Две явных: @NotBlank @Size(max=50) |
@Min на BigDecimal | R-VLD-STD-2 | @DecimalMin("...") |
@NotEmpty на строке | R-VLD-STD-1 | @NotBlank |
@Pattern для частого формата (телефон, ИНН) | R-VLD-STD-3 | Custom constraint (@RussianPhone) |
Куда дальше
- Validation → раздел 2. Стандартные constraints — нормативные формулировки
R-VLD-STD-*. - Custom constraints — когда стандартных не хватает.
- Где валидировать — где аннотации применяются.
- OpenAPI-сгенерированные DTO — как OpenAPI keywords ↔ Jakarta аннотации.
- Cross-field validation — для правил между несколькими полями.