Валидация входных данных: 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 строгим и самодокументируемым.