Опирается на правила: R-VLD-STD-1R-VLD-STD-5 и R-VLD-STD-X1R-VLD-STD-X3 из Validation Style Guide → раздел 2. Стандартные constraints.

Важно знать

  • @NotNull vs @NotBlank vs @NotEmpty — три разные аннотации для трёх типов: object/Boolean, String, коллекция. Не взаимозаменяемы.
  • @NotBlank для строки = «не null, не пустая, не из одних пробелов». Одна аннотация вместо тройки.
  • Числовые: @Min/@Max для примитивов и боксированных, @DecimalMin/@DecimalMax для BigDecimal/BigInteger. @Min на BigDecimal — антипаттерн.
  • @Email вместо кастомного regex. Jakarta учитывает RFC 5321/5322, обновляется с новыми версиями.
  • @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
) {}

Различия:

АннотацияПрименяется кЧто проверяет
@NotNullobject-поля, Boolean, любой тип-обёрткаvalue != null
@NotBlankStringvalue != null && !value.trim().isEmpty()
@NotEmptyString, Collection, Map, arrayvalue != 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: правило «validFromvalidUntil» — это уже 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 вместо @EmailR-VLD-STD-X2@Email
Composite-аннотация (@NotBlankAndAtMost50) поверх стандартныхR-VLD-STD-X3Две явных: @NotBlank @Size(max=50)
@Min на BigDecimalR-VLD-STD-2@DecimalMin("...")
@NotEmpty на строкеR-VLD-STD-1@NotBlank
@Pattern для частого формата (телефон, ИНН)R-VLD-STD-3Custom constraint (@RussianPhone)

Куда дальше

  • Validation → раздел 2. Стандартные constraints — нормативные формулировки R-VLD-STD-*.
  • Custom constraints — когда стандартных не хватает.
  • Где валидировать — где аннотации применяются.
  • OpenAPI-сгенерированные DTO — как OpenAPI keywords ↔ Jakarta аннотации.
  • Cross-field validation — для правил между несколькими полями.