Validierung in Spring Boot mit Bean Validation (@Valid, @NotBlank, …)
1. Überblick
Beim Entwickeln einer REST API müssen Sie immer die Eingabedaten validieren, bevor Sie die Geschäftslogik verarbeiten: E-Mail muss im richtigen Format sein, Namen dürfen nicht leer sein, Alter muss positiv sein… Bean Validation (JSR 380, implementiert durch Hibernate Validator) hilft Ihnen dabei, dies mit Annotations zu tun, statt zahlreiche if-else Abfragen manuell zu schreiben.
Dieser Artikel behandelt die Konfiguration, die Verwendung von @Valid zusammen mit häufigen Annotations, die Validierung verschachtelter Objekte, das Schreiben von Custom Validatoren und die Fehlerbehandlung, um benutzerfreundliche Responses an den Client zu liefern. Nach dem Lesen werden Sie sicher sein, Validierung für alle DTOs in Ihrem Spring Boot Projekt anzuwenden.
2. Abhängigkeit hinzufügen
Spring Boot bringt Bean Validation standardmäßig nicht mit; Sie müssen spring-boot-starter-validation hinzufügen:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Diese Abhängigkeit zieht Hibernate Validator mit sich – die am weitesten verbreitete Implementierung der Bean Validation API. Wenn Sie spring-boot-starter-web verwenden, denken viele, dass Validierung bereits enthalten ist, aber seit Spring Boot 2.3 wurde dieser Starter separiert – wenn er fehlt, wird @Valid keine Wirkung haben und keinen Fehler melden; ungültige Daten passieren einfach durch.
3. Request Body mit @Valid validieren
3.1. DTO mit Constraint Annotations deklarieren
public record CreateOrderRequest(
@NotBlank(message = "Kundenname darf nicht leer sein")
String customerName,
@Email(message = "E-Mail ist ungültig")
@NotBlank
String email,
@Positive(message = "Menge muss größer als 0 sein")
Integer quantity,
@DecimalMin(value = "0.0", inclusive = false, message = "Preis muss größer als 0 sein")
BigDecimal unitPrice
) {}
Jedes Feld hat eine oder mehrere Constraint Annotations: @NotBlank akzeptiert keine null/leeren/nur-Whitespace-Strings, @Email prüft das E-Mail-Format, @Positive erfordert positive Zahlen, @DecimalMin setzt einen Mindestwert. Der Parameter message ist der Fehlertext, der zurückgegeben wird, wenn die Constraint verletzt wird.
3.2. @Valid im Controller anwenden
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<String> createOrder(@Valid @RequestBody CreateOrderRequest request) {
// request ist garantiert gültig an dieser Stelle
return ResponseEntity.ok("Order created for " + request.customerName());
}
}
@Valid vor @RequestBody teilt Spring MVC mit, die Validierung des aus JSON deserialisierten Objekts durchzuführen. Wenn ein Feld gegen eine Constraint verstößt, wirft Spring MethodArgumentNotValidException bevor der Methodenrumpf ausgeführt wird – d.h. Sie müssen nicht selbst auf null oder leere Werte in der Geschäftslogik prüfen.
Senden Sie eine Request ohne customerName:
POST /api/orders
{
"email": "a@b.com",
"quantity": 2,
"unitPrice": 10.5
}
Standardmäßig (ohne Exception-Behandlung) gibt Spring HTTP 400 mit einem längeren und schwer lesbaren Body zurück. Abschnitt 5 zeigt, wie man dies sauberer handhabt.
4. Häufig verwendete Annotations
| Annotation | Bedeutung |
|---|---|
@NotNull |
Wert darf nicht null sein |
@NotBlank |
String darf nicht null, leer oder nur Whitespace sein |
@NotEmpty |
Collection/String/Array darf nicht null oder leer sein |
@Size(min, max) |
String-Länge oder Collection-Größe |
@Min / @Max |
Ganzzahlen-Limits |
@Positive / @Negative |
Positive/negative Zahlen |
@Email |
E-Mail-Format |
@Pattern(regexp) |
Regex-Abgleich |
@Past / @Future |
Datum in der Vergangenheit/Zukunft |
Beispiel mit mehreren Constraints auf einem Feld:
public class Customer {
@Size(min = 3, max = 50, message = "Name muss zwischen 3 und 50 Zeichen lang sein")
private String name;
@Pattern(regexp = "^0\\d{9}$", message = "Telefonnummer muss 10 Ziffern haben und mit 0 beginnen")
private String phone;
@Past(message = "Geburtsdatum muss in der Vergangenheit liegen")
private LocalDate birthDate;
}
Sie können mehrere Annotations auf demselben Feld kombinieren – z.B. @NotBlank zusammen mit @Size – um gleichzeitig leere Strings zu verhindern und die Länge zu begrenzen. Der Validator führt alle Constraints aus und sammelt die Fehler, statt bei dem ersten Fehler zu stoppen.
5. Fehlerbehandlung mit @ExceptionHandler
Statt Spring die Standard-Response mit verworrenen Details zuzulassen, verwenden Sie @RestControllerAdvice zur Formatierung:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.badRequest().body(errors);
}
}
MethodArgumentNotValidException enthält BindingResult mit einer Liste von FieldError – jeder Fehler hat einen Feldnamen und eine Nachricht. Der obige Code sammelt alle Fehler in einer Map<Feldname, Nachricht> und gibt HTTP 400 zurück.
Ergebnis beim erneuten Aufruf der obigen Request ohne customerName:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"customerName": "Kundenname darf nicht leer sein"
}
Die Response ist jetzt knapp und prägnant; der Client kann Fehler leicht den Formularfeldern zuordnen.
6. Validierung verschachtelter Objekte und Collections
6.1. Verschachtelte Objekte mit @Valid
public record AddressRequest(
@NotBlank String city,
@NotBlank String street
) {}
public record CustomerRequest(
@NotBlank String name,
@Valid
@NotNull
AddressRequest address
) {}
Wenn CustomerRequest.address nur @NotNull hat, ohne @Valid, werden die Constraints in AddressRequest (wie @NotBlank auf city) nicht überprüft – dies ist ein häufiger Fehler bei neuen Benutzern von Bean Validation. @Valid hier weist den Validator an, “tief in das” Objekt zu gehen.
6.2. Validierung jedes Elements in einer List
public record BulkOrderRequest(
@NotEmpty(message = "Item-Liste darf nicht leer sein")
@Valid
List<@Valid OrderItem> items
) {}
public record OrderItem(
@NotBlank String productCode,
@Positive Integer quantity
) {}
@Valid vor List<...> (oder im Type Argument) weist den Validator an, jedes Element in der List zu prüfen, nicht nur ob die List null ist. Wenn Element 2 in der List quantity = -1 hat, wird der zurückgegebene Fehler das Feld als items[1].quantity angeben.
7. Custom Validator schreiben
Wenn vordefinierte Constraints nicht ausreichen, erstellen Sie Ihre eigene Annotation. Beispiel: Validierung eines Bestellcodes, der mit ORD- beginnen muss:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderCodeValidator.class)
public @interface ValidOrderCode {
String message() default "Bestellcode muss das Format ORD-xxxxx haben";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class OrderCodeValidator implements ConstraintValidator<ValidOrderCode, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // Lasse @NotNull null-Werte separat validieren, um doppelte Fehler zu vermeiden
}
return value.matches("^ORD-\\d{5}$");
}
}
@Constraint(validatedBy = ...) verbindet die Annotation mit der Implementierungsklasse. ConstraintValidator<A, T> nimmt A als Annotation und T als Feldtyp an. Die Methode isValid gibt true zurück, wenn gültig. Tipp: isValid sollte true zurückgeben, wenn value == null, da die Null-Validierung @NotNull überlassen sollte – so vermeiden Sie, dass ein Feld mit zwei verschiedenen Fehlermeldungen gemeldet wird.
Verwendung im DTO:
public record OrderLookupRequest(
@NotBlank
@ValidOrderCode
String orderCode
) {}
8. Validierung mit JUnit 5 + AssertJ testen
Sie können Validator direkt testen, ohne den Spring MVC Context zu starten – das ist schneller als Integration Tests:
class CreateOrderRequestValidationTest {
private static Validator validator;
@BeforeAll
static void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
validator = factory.getValidator();
}
}
@Test
void shouldRejectBlankCustomerName() {
CreateOrderRequest request = new CreateOrderRequest(
"", "a@b.com", 2, BigDecimal.TEN);
Set<ConstraintViolation<CreateOrderRequest>> violations = validator.validate(request);
assertThat(violations)
.extracting(v -> v.getPropertyPath().toString())
.contains("customerName");
}
@Test
void shouldPassWhenAllFieldsValid() {
CreateOrderRequest request = new CreateOrderRequest(
"Nguyen Van A", "a@b.com", 2, BigDecimal.TEN);
Set<ConstraintViolation<CreateOrderRequest>> violations = validator.validate(request);
assertThat(violations).isEmpty();
}
}
Validation.buildDefaultValidatorFactory() erstellt einen unabhängigen Validator, ohne Spring Context zu brauchen. validator.validate(request) gibt ein Set<ConstraintViolation> zurück – leer bedeutet gültig. Diese Testmethode eignet sich zum Unit Testing einzelner DTOs, getrennt von Controller-Logik.
9. Path Variable und Query Parameter validieren
@Valid bei @RequestBody ist am häufigsten, aber Sie können auch einzelne Parameter mit @Validated auf der Controller-Klasse validieren:
@RestController
@RequestMapping("/api/orders")
@Validated
public class OrderQueryController {
@GetMapping("/{id}")
public ResponseEntity<String> getOrder(
@PathVariable @Positive(message = "ID muss größer als 0 sein") Long id) {
return ResponseEntity.ok("Order " + id);
}
}
@Validated (von Spring, anders als @Valid von Bean Validation) aktiviert die Validierung auf Methodenparameter-Ebene. Wenn id = -1, wirft Spring ConstraintViolationException (anders als MethodArgumentNotValidException aus Abschnitt 5) – Sie brauchen einen separaten @ExceptionHandler für diese Exception:
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraintViolation(ConstraintViolationException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
Hinweis: Diese zwei Exceptions sind leicht zu verwechseln –
MethodArgumentNotValidExceptionist für@Valid @RequestBody/@ModelAttribute, währendConstraintViolationExceptionfür@Validatedauf Path Variable/Query Parameter gedacht ist. Wenn einer der beiden Handler fehlt, fällt der Fehler zu einer unangenehmen HTTP 500 Standard-Response zurück.
10. Häufige Fehler und wann man alternatives erwägen sollte
- @Valid vergessen: Constraint Annotations aktivieren sich nicht selbst; Sie müssen
@Validan der richtigen Stelle deklarieren (Controller-Parameter, verschachteltes Feld, Type Argument eines Generics). - Abhängigkeit starter-validation fehlt: Constraint Annotations werden stillschweigend ignoriert, es wird keine Exception geworfen – sehr schwer zu erkennen, wenn Sie nicht testen.
- Komplexe Validierungslogik über mehrere Felder (z.B. “Enddatum muss nach Startdatum liegen”): Bean Validation kann das über Class-Level Constraints handhaben, aber wenn die Logik von Datenbankdaten abhängt (z.B. “E-Mail existiert nicht bereits”), sollten Sie es in der Service-Schicht handhaben – Validatoren sollten keine Repository aufrufen, da dies sie schwer testbar macht und gegen das Single-Responsibility-Prinzip verstößt.
- Mehrsprachige Fehlermeldungen: Verwenden Sie
message = "{order.code.invalid}"und einemessages.propertiesDatei statt die Nachrichten hardcodiert in Annotations zu schreiben, wenn Ihr Projekt i18n benötigt.
11. Fazit
Bean Validation ermöglicht es Ihnen, Validierungsregeln direkt auf DTOs mit Annotations zu deklarieren, statt if-else Code überall zu verstreuen. Kombiniert mit @Valid/@Validated und @RestControllerAdvice haben Sie einen konsistenten Validierungs- und Fehlermeldemechanismus für Ihre gesamte API. Wenn vordefinierte Constraints nicht ausreichen, ermöglichen Custom Validator eine flexible Erweiterung, während Sie den deklarativen Stil mit seiner Lesbarkeit und Testbarkeit beibehalten.