← Назад к вопросам

Что такое валидация на уровне контроллера?

2.0 Middle🔥 191 комментариев
#SOLID и паттерны проектирования#Spring Boot и Spring Data

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Валидация на уровне контроллера

Валидация на уровне контроллера — это проверка корректности входных данных, которая происходит в слое контроллера (в точке входа запроса в приложение). Это первая линия защиты, которая гарантирует, что только валидные данные попадают в бизнес-логику приложения.

Архитектура валидации

В чистой архитектуре валидация происходит на нескольких уровнях:

  1. Контроллер — базовая валидация входных параметров
  2. DTO (Data Transfer Object) — декларативная валидация через аннотации
  3. Domain / Service — бизнес-логика валидация (invariants)
  4. База данных — constraints на уровне схемы

Стандартный способ с использованием Bean Validation

import javax.validation.constraints.*;

public class UserRegistrationRequest {
    @NotBlank(message = "Username не может быть пустым")
    @Size(min = 3, max = 50, message = "Username должен быть от 3 до 50 символов")
    private String username;
    
    @NotBlank(message = "Email не может быть пустым")
    @Email(message = "Email должен быть корректным")
    private String email;
    
    @NotBlank(message = "Пароль не может быть пустым")
    @Size(min = 8, message = "Пароль должен быть минимум 8 символов")
    @Pattern(
        regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
        message = "Пароль должен содержать заглавные, строчные буквы и цифры"
    )
    private String password;
    
    @NotNull(message = "Возраст не может быть null")
    @Min(value = 18, message = "Возраст должен быть минимум 18")
    @Max(value = 120, message = "Возраст должен быть максимум 120")
    private Integer age;
    
    // Getters, setters
    public String getUsername() { return username; }
    public String getEmail() { return email; }
    public String getPassword() { return password; }
    public Integer getAge() { return age; }
}

Использование в Spring контроллере

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.validation.Valid;

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping("/register")
    public ResponseEntity<UserResponse> register(@Valid @RequestBody UserRegistrationRequest request) {
        // Валидация происходит АВТОМАТИЧЕСКИ перед вызовом метода
        // Если данные невалидны, Spring вернёт 400 Bad Request с ошибками
        
        UserResponse response = userService.registerUser(request);
        return ResponseEntity.status(201).body(response);
    }
}

Обработка ошибок валидации

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(
                error.getField(),
                error.getDefaultMessage()
            )
        );
        
        return ResponseEntity
            .status(400)
            .body(new ErrorResponse(
                "Validation failed",
                400,
                errors
            ));
    }
}

Пример ответа при ошибке валидации

{
  "message": "Validation failed",
  "status": 400,
  "errors": {
    "username": "Username должен быть от 3 до 50 символов",
    "email": "Email должен быть корректным",
    "password": "Пароль должен содержать заглавные, строчные буквы и цифры",
    "age": "Возраст должен быть минимум 18"
  }
}

Кастомная валидация в контроллере

@PostMapping("/create-order")
public ResponseEntity<OrderResponse> createOrder(
        @Valid @RequestBody CreateOrderRequest request) {
    
    // Базовая валидация через @Valid выполнена
    
    // Дополнительная кастомная валидация
    if (request.getDeliveryDate().isBefore(LocalDate.now())) {
        throw new BadRequestException("Дата доставки не может быть в прошлом");
    }
    
    if (request.getItems().isEmpty()) {
        throw new BadRequestException("Заказ должен содержать минимум один товар");
    }
    
    OrderResponse response = orderService.createOrder(request);
    return ResponseEntity.status(201).body(response);
}

Кастомные аннотации валидации

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.annotation.*;

// Аннотация
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
    String message() default "Некорректный номер телефона";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Реализация валидатора
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;  // @NotNull обработает null
        }
        
        // Проверка формата номера телефона
        return value.matches("^\\+?[1-9]\\d{1,14}$");  // E.164 формат
    }
}

// Использование
public class ContactRequest {
    @NotBlank
    @ValidPhoneNumber
    private String phoneNumber;
}

Валидация коллекций

import javax.validation.constraints.NotEmpty;

public class BatchUserRequest {
    @NotEmpty(message = "Список пользователей не может быть пустым")
    @Valid  // Валидирует каждый элемент списка
    private List<UserRegistrationRequest> users;
    
    public List<UserRegistrationRequest> getUsers() { return users; }
}

@PostMapping("/batch-register")
public ResponseEntity<List<UserResponse>> batchRegister(
        @Valid @RequestBody BatchUserRequest request) {
    // Валидируется весь список и каждый объект в списке
    List<UserResponse> responses = userService.registerBatch(request.getUsers());
    return ResponseEntity.status(201).body(responses);
}

Валидация Query параметров

@GetMapping("/users")
public ResponseEntity<List<UserResponse>> getUsers(
        @RequestParam(required = false)
        @Min(value = 1, message = "Страница должна быть >= 1")
        Integer page,
        
        @RequestParam(required = false)
        @Min(value = 1, message = "Размер страницы должен быть >= 1")
        @Max(value = 100, message = "Размер страницы не может быть > 100")
        Integer size,
        
        @RequestParam(required = false)
        @Pattern(regexp = "^(name|date|id)$", message = "Некорректное поле для сортировки")
        String sortBy) {
    
    return ResponseEntity.ok(userService.getUsers(page, size, sortBy));
}

Валидация Path параметров

@GetMapping("/users/{id}")
public ResponseEntity<UserResponse> getUser(
        @PathVariable
        @Min(value = 1, message = "ID должен быть положительным числом")
        Long id) {
    
    UserResponse response = userService.getUserById(id);
    return ResponseEntity.ok(response);
}

Niveles валидации

// Группы валидации для разных сценариев
public class ValidationGroups {
    public interface Create {}
    public interface Update {}
}

public class UserRequest {
    @NotBlank(groups = {ValidationGroups.Create.class})
    @Null(groups = {ValidationGroups.Update.class})
    private Long id;
    
    @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
    private String email;
}

@PostMapping
public ResponseEntity<UserResponse> create(
        @Valid(groups = ValidationGroups.Create.class)
        @RequestBody UserRequest request) {
    // Валидирует в соответствии с группой Create
    return ResponseEntity.status(201).body(userService.create(request));
}

@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(
        @PathVariable Long id,
        @Valid(groups = ValidationGroups.Update.class)
        @RequestBody UserRequest request) {
    // Валидирует в соответствии с группой Update
    return ResponseEntity.ok(userService.update(id, request));
}

Лучшие практики

  1. Используй @Valid/@Validated для автоматической валидации
  2. Декларативная валидация через аннотации проще, чем программная
  3. Кастомные аннотации для повторяющейся валидации
  4. Группы валидации для разных сценариев (Create/Update)
  5. Детальные сообщения об ошибках помогают клиентам
  6. Обрабатывай исключения в GlobalExceptionHandler
  7. Не полагайся только на контроллер — добавь валидацию на уровне domain

Валидация на уровне контроллера — это критичный слой, который предотвращает попадание некорректных данных в остальную систему и обеспечивает безопасность приложения.