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 – MethodArgumentNotValidException ist für @Valid @RequestBody/@ModelAttribute, während ConstraintViolationException für @Validated auf 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 @Valid an 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 eine messages.properties Datei 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.

spring-bootvalidationbean-validationjavarest-api