Wstęp
Walidacja danych w API to jeden z tych tematów, o których najczęściej przypominamy sobie dopiero wtedy, gdy użytkownik zaczyna wprowadzać nieoczekiwane wartości. Brak wymaganych pól, za długie ciągi znaków, niepoprawne formaty dat czy e-maile powodują błędy, które nie powinny nigdy dotrzeć do logiki biznesowej. Na szczęście Spring Boot oferuje gotowy i bardzo wygodny sposób walidowania danych wejściowych.
W tym wpisie z serii „Szybki Strzał” pokażę Ci, jak używać Bean Validation w Springu – od podstawowych adnotacji, przez walidację w kontrolerach, po obsługę błędów i tworzenie własnych reguł.
Czym jest walidacja i dlaczego jej potrzebujesz?
Walidacja to nic innego jak sprawdzenie, czy dane trafiające do Twojej aplikacji mają sens i spełniają ustalone wymagania. Brzmi prosto, ale w praktyce to fundament stabilnego API – bez niej prędzej czy później trafisz na błędy, które nigdy nie powinny się wydarzyć.
Możesz mieć świetną walidację na froncie, ale nie możesz traktować jej jako zabezpieczenia. Użytkownik zawsze może ominąć interfejs, wysłać request bezpośrednio czy spróbować coś kombinować. Dlatego backend musi być ostatnią linią obrony i to on powinien pilnować poprawności danych.
Brak sensownej walidacji oznacza otwarte drzwi do problemów: puste pola, niepoprawne formaty, przypadkowe wartości czy zagrożenia bezpieczeństwa. Spring Boot w połączeniu z Bean Validation pozwala ogarnąć to naprawdę wygodnie – kilka adnotacji i masz pewność, że do logiki biznesowej trafiają tylko dane, które nadają się do przetworzenia.

Zależności i DTO
Zacznijmy od podstaw. W wersjach Spring Boota od 2.3 w górę, biblioteki walidacyjne nie są już domyślnie „zaszyte” w starterze spring-boot-starter-web. Musimy dodać je jawnie.
Jeśli używasz Mavena, dodaj do pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Natomiast jeśli używasz Gradle to w build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-validation'
Pod spodem Spring dociągnie Hibernate Validator, który jest referencyjną implementacją standardu Jakarta Bean Validation.
Dlaczego walidujemy DTO, a nie Encje?
Najwygodniej i najczyściej waliduje się dane tam, gdzie faktycznie do nas trafiają – czyli w kontrolerze, na DTO. To ten obiekt powinien sprawdzić, czy request w ogóle ma sens, zanim zaczniemy cokolwiek robić dalej. Dzięki temu serwis nie musi walczyć z pustymi polami, dziwnymi wartościami czy formatami, które nigdy nie powinny przejść.
Encje JPA też można walidować, ale zwykle mają one inne zadanie. Trzymają pełny model danych i często zawierają rzeczy, których użytkownik nawet nie powinien widzieć. Dlatego stosuję je raczej jako dodatkową blokadę przed zapisem do bazy, a nie jako główne miejsce walidacji.
W praktyce najlepiej sprawdza się prosty układ: DTO waliduje wejście, a serwis dostaje już dane, które nadają się do użycia.
Walidacja danych w Springu
Bean Validation dostarcza szereg gotowych adnotacji, które możesz użyć na polach klasy:
- @NotNull – pole nie może być null
- @NotBlank – pole typu String nie może być puste (null, pusty String, same spacje)
- @Size(min = x, max = y) – określa minimalną i maksymalną długość
- @Email – sprawdza poprawność formatu email
- @Min(value) / @Max(value) – wartość musi być większa/mniejsza od podanej
- @Pattern(regexp) – wartość musi pasować do wyrażenia regularnego
- @Positive / @Negative – liczba musi być dodatnia/ujemna
Zobaczmy też na przykładzie jak to wygląda. Stworzymy prostą klasę DTO ze szczegółami o użytkowniku:
import jakarta.validation.constraints.*;
public class UserRegistrationDto {
@NotBlank(message = "Imię jest wymagane")
private String firstName;
@NotBlank(message = "Nazwisko jest wymagane")
private String lastName;
@Email(message = "Niepoprawny format adresu email")
@NotBlank(message = "Email nie może być pusty")
private String email;
@Min(value = 18, message = "Musisz mieć co najmniej 18 lat")
private int age;
// gettery i settery
}
Warto tutaj zaznaczyć, że używamy pakietu jakarta.validation.constraints dostępnego od Spring Boot 3+.
Walidacja w kontrolerach z @Valid i @Validated
Samo dodanie adnotacji do DTO nic nie da, jeśli nie powiemy Springowi, żeby je sprawdził. Dzieje się to w kontrolerze przy pomocy adnotacji @Valid (z pakietu jakarta.validation) lub @Validated (ze Springa). W prostych przypadkach działają one zamiennie, ale standardem jest użycie @Valid przy argumentach metody.
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserResponse> register(@Valid @RequestBody UserRegistrationDto dto) {
// Jeśli kod dotarł tutaj, to znaczy, że dto jest poprawne!
// Nie musisz robić if (dto.getEmail() == null)...
UserResponse response = userService.register(dto);
return ResponseEntity.ok(response);
}
}
Co się stanie, gdy walidacja nie przejdzie?
Jeśli wyślesz JSON-a z pustym imieniem, Spring nawet nie dopuści żądania do metody register. Walidacja zadziała wcześniej i poleci MethodArgumentNotValidException. To dobry znak – kontroler dostaje tylko dane, które mają sens.
Spring Boot domyślnie odpowie statusem 400 Bad Request i dość szczegółowym komunikatem o błędach (zależnie od tego, jak masz ustawione server.error.include-message). W praktyce jednak rzadko chcemy zdawać się na domyślny format. Frontend, mobilka czy klient API powinny dostać jasną, przewidywalną odpowiedź – i właśnie dlatego przechwytujemy wyjątek i sami budujemy strukturę błędu.
Obsługa błędów: Globalny Exception Handler
Zamiast pozwalać Springowi na wyświetlanie domyślnych błędów, przechwyćmy ten konkretny wyjątek i zamieńmy go na czytelną mapę błędów: pole -> komunikat (dużo bardziej na temat globalnej obsługi błędów rozpisałem się we wpisie na temat tworzenia własnych wyjątów – w tym wpisie skupiam się tylko w kontekście walidacji).
Wykorzystamy do tego mechanizm @ControllerAdvice.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
}
Dzięki temu, gdy użytkownik wyśle błędne dane, otrzyma czystą odpowiedź JSON:
{
"email": "Niepoprawny format adresu email",
"age": "Musisz mieć co najmniej 18 lat"
}
To znacznie ułatwia pracę frontendowcom, którzy mogą wyświetlić te komunikaty bezpośrednio pod polami formularza.
Walidacja na poziomie serwisu
Czasami dane nie przychodzą z kontrolera REST. Mogą pochodzić z kolejki (np. RabbitMQ, Kafka), z importu pliku lub z innego serwisu. Wtedy @Valid w kontrolerze nam nie pomoże.
Aby wymusić walidację wewnątrz warstwy biznesowej (w Service), musimy:
- Oznaczyć klasę serwisu adnotacją
@Validated(to ważne – to adnotacja Springowa, która aktywuje walidację AOP). - Dodać
@Validlub konkretne constrainty przy parametrach metod.
@Service
@Validated // 1. Aktywacja walidacji dla tej klasy
public class UserService {
public void register(@Valid UserRegistrationDto dto) {
// 2. Wymuszenie sprawdzenia DTO
// logika biznesowa...
}
public void updateEmail(@NotNull @Email String newEmail) {
// Możemy też walidować pojedyncze parametry proste
// logika...
}
}
Uwaga na wyjątki! W tym przypadku Spring nie rzuci MethodArgumentNotValidException, ale ConstraintViolationException. Jeśli używasz globalnego handlera, musisz obsłużyć również ten wyjątek, jeśli chcesz mieć spójne odpowiedzi błędów dla całej aplikacji.
Własna adnotacja walidująca
Czasami standardowe adnotacje nie wystarczają. Załóżmy, że chcesz sprawdzić, czy numer telefonu ma konkretny format albo czy login nie jest na liście zarezerwowanych słów.
W takich przypadkach możesz stworzyć własną adnotację walidacyjną.
Na początek stwórzmy adnotację:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
String message() default "Niepoprawny numer telefonu";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Następnie musimy zaimplementować logikę walidacji dla tej adnotacji:
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // @NotNull obsłuży null
}
return value.matches("\\+?[0-9]{9,15}");
}
}
A na koniec użyjmy naszego rozwiązania na polu:
public class UserRegistrationDto {
@ValidPhoneNumber(message = "Numer telefonu musi mieć od 9 do 15 cyfr")
private String phoneNumber;
// inne pola
}
Podsumowanie
Walidacja danych w Springu jest naprawdę prosta, jeśli podejdziesz do niej z głową. Bean Validation i adnotacje typu @NotBlank, @Email czy @Size pozwalają ogarnąć większość przypadków bez pisania zbędnego kodu. Dodajesz @Valid w kontrolerze, przechwytujesz błędy w @RestControllerAdvice i masz solidne, kompletne rozwiązanie.
A jeśli trafisz na przypadek, którego nie da się obsłużyć gotowymi adnotacjami, nic nie stoi na przeszkodzie, żeby stworzyć własną. To kilka linijek kodu, a potrafi zaoszczędzić sporo nerwów i znacząco uprościć logikę.
Jeżeli chcesz wejść krok dalej, warto spojrzeć w kierunku walidacji grupowej (@Validated z grupami) albo walidacji obiektów zagnieżdżonych przy pomocy @Valid na polach klasy. To przydatne narzędzia, szczególnie kiedy model danych zaczyna się rozrastać.