Валидация входных данных: Bean Validation и @Valid

Нельзя доверять данным клиента. Bean Validation проверяет их декларативно: аннотации на полях DTO + @Valid в контроллере — и невалидный запрос отсекается до бизнес-логики.
Суть: повесьте @NotBlank, @Email, @Size на поля DTO и @Valid на параметр контроллера. Spring проверит данные и при нарушении вернёт 400 с описанием ошибок.

Первое правило бэкенда: данные от клиента ненадёжны. Пустое имя, кривой email, отрицательный возраст — всё это придёт рано или поздно. Можно писать проверки руками в каждом методе, но это шумно и легко забыть. Bean Validation выносит правила на сами поля.

Аннотации на DTO

import jakarta.validation.constraints.*;

public record CreateUserRequest(

    @NotBlank(message = "Имя обязательно")
    @Size(max = 100, message = "Не длиннее 100 символов")
    String name,

    @NotBlank @Email(message = "Некорректный email")
    String email,

    @Min(value = 0, message = "Возраст не может быть отрицательным")
    @Max(150)
    int age
) { }

Аннотации из jakarta.validation.constraints описывают правила прямо на полях. Самые частые: @NotNull, @NotBlank (строка не пустая), @Email, @Size, @Min/@Max, @Pattern.

Запуск проверки через @Valid

@PostMapping("/api/users")
public ResponseEntity<UserResponse> create(
        @Valid @RequestBody CreateUserRequest request) {
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(service.create(request));
}

@Valid перед @RequestBody запускает проверку. Если правила нарушены, Spring бросает MethodArgumentNotValidException ещё до тела метода — бизнес-логика не выполнится с мусором.

Как работает под капотом

Увидев @Valid, Spring передаёт объект валидатору (Hibernate Validator). Тот проходит по полям, проверяет каждое правило и собирает список нарушений. Если он не пуст — вылетает исключение, которое глобальный обработчик превращает в ответ 400 с перечнем ошибок.

  POST /api/users  тело JSON
        |
        v
  @Valid -> Hibernate Validator
        |
        +-- name пуст?   -> нарушение
        +-- email кривой?-> нарушение
        +-- age < 0?      -> нарушение
        |
   нарушения есть? --да--> MethodArgumentNotValidException -> 400
        |
        нет
        v
   метод контроллера выполняется

Смоделируем валидатор, собирающий все нарушения:

# Декларативная валидация: правила -> список нарушений
def validate(dto):
    errors = []
    if not dto.get("name", "").strip():
        errors.append("name: имя обязательно")
    if len(dto.get("name", "")) > 100:
        errors.append("name: не длиннее 100 символов")
    if "@" not in dto.get("email", ""):
        errors.append("email: некорректный email")
    if dto.get("age", 0) < 0:
        errors.append("age: возраст не может быть отрицательным")
    return errors

good = {"name": "Анна", "email": "[email protected]", "age": 30}
bad = {"name": "", "email": "не-почта", "age": -5}

print("good ->", validate(good) or "OK (200)")
print("bad  ->", validate(bad), "-> 400")

Нажмите «Попробуй сам ▶»: валидатор собирает все ошибки сразу, а не падает на первой — клиент видит полную картину.

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

  • Забыть @Valid. Без него аннотации на DTO просто игнорируются — проверки не будет.
  • @NotNull вместо @NotBlank для строк. @NotNull пропустит пустую строку ""; для строк нужен @NotBlank.
  • Валидация в сервисе вместо границы. Проверяйте на входе (контроллер), чтобы мусор не доходил до логики.

Best practices

  • Вешайте ограничения на поля DTO и ставьте @Valid на входной параметр контроллера.
  • Возвращайте понятные сообщения через message и единый формат ошибок (ProblemDetail).
  • Для строк используйте @NotBlank, для коллекций — @NotEmpty, для чисел — @Min/@Max.

Итог: Bean Validation отсекает плохие данные декларативно. Аннотации на DTO + @Valid в контроллере = автоматическая проверка и понятный ответ 400. Бизнес-логика получает только чистый ввод.

Закрепим главное

Валидация на границе приложения — это первая линия обороны. Чем раньше вы отсечёте некорректные данные, тем меньше мусора доберётся до бизнес-логики и базы. Декларативный подход Bean Validation выносит правила на сами поля DTO, поэтому они видны рядом с данными и не теряются среди процедурного кода проверок.

Зафиксируйте два частых промаха. Первый — забытый @Valid: без него аннотации на полях остаются просто разметкой, валидатор не запускается, и невалидные данные спокойно проходят дальше. Аннотации описывают правила, но запускает проверку именно @Valid на параметре контроллера. Второй — выбор аннотации для строк: @NotNull пропустит пустую строку и строку из пробелов, поэтому для текстовых полей нужен @NotBlank, требующий непустого содержимого. Для коллекций аналогично используйте @NotEmpty, а для чисел — диапазоны @Min/@Max. Правильно подобранное ограничение делает контракт API строгим и самодокументируемым.

Проверьте себя
1. Что произойдёт, если повесить @NotBlank на поле DTO, но забыть @Valid в контроллере?
AЗапрос отклонится с ошибкой 400
BВалидация не запустится — аннотации проигнорируются
CПриложение не скомпилируется
DПоле станет необязательным
2. Почему для проверки непустой строки используют @NotBlank, а не @NotNull?
A@NotNull устарел
B@NotNull пропустит пустую строку \"\" и пробелы, а @NotBlank требует непустого содержимого
CОни идентичны для строк
D@NotBlank работает только с числами