1. Verwechslung von @Valid und @Validated
Wenn Sie schon einmal gesehen haben, wie ein Anfänger alle möglichen Annotationen auf eine Klasse setzt – @Component, @Service, @Transactional, @Slf4j, @Validated und „jetzt wird es bestimmt funktionieren“ – dann haben Sie klassischen Annotations-Folklore erlebt. @Valid und @Validated – ein ideales Paar für solche Verwirrung: die Namen ähneln sich, der Zweck ist verwandt, und die Folgen eines Fehlers zeigen sich nicht sofort.
Die Verwirrung beginnt normalerweise mit einer einfachen Situation: Sie haben ehrlich Constraints auf das DTO geschrieben (z. B. @NotBlank auf title), eine Anfrage mit leerem title gesendet … und plötzlich hat der Service gearbeitet, als ob nichts passiert wäre. Im Kopf des Entwicklers entsteht der Gedanke: „Wahrscheinlich mag Spring mich einfach nicht.“
In Wahrheit hat Spring damit nichts zu tun (es hat ohnehin schon komplizierte Beziehungen zum Leben). Meistens liegt das Problem darin, dass wir zwei Rollen vermischt haben:
- Die erste Rolle – die Regeln beschreiben (Constraints auf dem DTO). Das ist „welche Eingabe zulässig ist“ im Rahmen des Vertrags.
- Die zweite Rolle – die Validierung an der richtigen Stelle im Pipeline aktivieren (normalerweise im Controller). Das ist „prüfe dieses Objekt genau jetzt“.
@Valid und @Validated sind an der zweiten Rolle beteiligt, aber auf unterschiedliche Weise. Wenn Sie ihren Zweck oder Einsatzort verwechseln, erhalten Sie den klassischen Bug: „Die Validierung funktioniert nicht, aber es gibt keinen Fehler.“
Wo sind die Regeln und der „Startknopf“
Es ist sehr hilfreich, sich ein Bild zu machen: Constraints auf einem DTO sind nicht „Code, der ausgeführt wird“, sondern eher „Aufkleber auf einer Kiste“. Solange niemand die Kiste öffnet und die Aufkleber liest, passiert nichts. Die Validierung wird nicht allein dadurch gestartet, dass Sie das DTO schön annotiert haben. Es braucht einen Auslöser.
In unserem Projekt ist die Regel einfach und gesund: Die Regeln leben im Request-DTO, denn das Request-DTO ist Teil des öffentlichen Vertrags. Wenn der Client einen title sendet, sollte genau das Request-DTO ehrlich sagen: „title ist erforderlich, Länge 3..120“. Nicht der Service, nicht der Controller, kein zufälliges if in einer Ecke des Repositories.
Der Start der Validierung (der besagte Auslöser) lebt dagegen dort, wo das DTO ins System eintritt – in der Controller-Methode. Diese Stelle ähnelt einem Drehkreuz in der U-Bahn: Sie können noch so viele Regeln aufstellen „ohne Fahrkarte nicht erlaubt“, aber wenn das Drehkreuz ausgeschaltet ist, kommen die Leute trotzdem durch.
In Spring MVC übernimmt meist @Valid am Methodenparameter des Controllers die Rolle des „Drehkreuzes“. Und das ist eine wichtige psychologische Trennung: Constraints beschreiben den Vertrag, @Valid aktiviert die Ausführung dieses Vertrags am Eingang.
2. @Valid in Spring MVC für @RequestBody
@Valid ist eine Standard-Annotation aus Bean Validation (in der modernen Welt Jakarta Validation). Sie ist keine „Spring-eigene“, sondern allgemein in der Java-Ökosystem verbreitet, und genau das macht sie praktisch: Wenn Sie @Valid sehen, wissen Sie, dass gleich eine Objektvalidierung anhand der Constraints stattfindet.
In Spring MVC für das @RequestBody-Szenario ist die Regel sehr einfach: Wenn Sie möchten, dass das Request-DTO automatisch validiert wird, setzen Sie @Valid auf den Parameter. Nachdem der HttpMessageConverter das JSON in ein DTO gelesen hat, bittet Spring den Validator, das Objekt zu prüfen. Wenn es Verletzungen gibt, sollte die Anfrage nicht in die Geschäftslogik gelangen.
Der häufigste „hinterhältige“ Punkt in unserem Baseline (Spring Boot 4) sind die Import-Pakete. Alte Tutorials zeigen ständig javax.validation.Valid, aber in der Jakarta-Welt ist das nicht mehr richtig.
Hier ist der korrekte Import für Boot 4:
import jakarta.validation.Valid; // ✅ korrektes Paket für Spring Boot 4
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PutMapping;
@PutMapping("/{taskId}")
public void update(@PathVariable String taskId, @Valid @RequestBody TaskPutRequest request) {
// @Valid ist der "Startknopf": wenn request die Constraints nicht erfüllt, wird die Methode nicht erreicht
}
Und hier ist ein typisches „Erbstück“ aus alten Beispielen, das in einem modernen Projekt entweder nicht kompiliert oder Sie eine halbe Stunde suchen lässt (fast wie Meditation, aber meist ohne Erleuchtung):
import javax.validation.Valid; // ❌ altes Paket, in Boot 4 so nicht verwenden
public void update(@PathVariable String taskId, @Valid @RequestBody TaskPutRequest request) {
// Selbst wenn das Projekt kompiliert (unwahrscheinlich), schleppen Sie einen inkompatiblen Import mit
}
Damit @Valid überhaupt einen Sinn ergibt, muss das DTO Constraints besitzen. Wir werden heute noch nicht zu einem Nachschlagewerk für @NotBlank und @Size, aber ein kleines Beispiel „für das Gefühl“ ist nützlich:
import jakarta.validation.constraints.NotBlank;
public record TaskCreateRequest(
@NotBlank // title ist erforderlich: weder leer noch nur Leerzeichen
String title
) {
}
Der Kern: @Valid ist der Befehl „prüfe dieses Objekt“. Was genau geprüft wird, legen die Constraints auf dem DTO fest.
Und ein separat wichtiger Punkt, der Nerven spart: @Valid im Controller heilt kein malformed JSON. Wenn das JSON syntaktisch kaputt ist, wird kein DTO erstellt, und die Bean Validation kommt nicht zum Zug. @Valid arbeitet mit dem Objekt, nicht mit dem JSON-Text. Das ist wie eine Auto-Hauptuntersuchung: Sie beginnt, wenn das Auto bereits in der Werkstatt steht, nicht wenn Sie es noch mit einem Bleistift auf Papier zeichnen.
3. @Validated in Spring und seine Rolle
@Validated ist eine Annotation von Spring (org.springframework.validation.annotation.Validated). Man begegnet ihr sehr oft, und das ist der Grund für die Falle: es scheint, als wäre das nur ein „zweiter Name“ für Validierung, also kann man sie überall statt @Valid verwenden. In der Praxis hat @Validated eine eigene Rolle, und deshalb darf man sie nicht als Kopie betrachten.
Wenn @Valid „prüfe dieses konkrete Objekt“ bedeutet, dann bedeutet @Validated „aktiviere die Spring-Integration der Validierung für Methoden/Komponente“. Sie ist besonders nützlich in Szenarien, in denen die Validierung nicht nur den Request-Body, sondern auch Methodenaufrufe betrifft (und fortgeschrittenere Prüfmodi). Diese Szenarien werden wir heute bewusst nicht entfalten, aber es ist wichtig, nicht zu verwechseln, wo @Validated tatsächlich hilft und wo es nur ein falsches Sicherheitsgefühl erzeugt.
Hier ist ein Beispiel, das man sehr gerne „für alle Fälle“ macht:
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Validated // Aktiviert die Spring-Validierung für Methoden/Komponente (nicht gleichbedeutend mit "validiert jeden @RequestBody automatisch")
@RestController
@RequestMapping("/api/v1/tasks")
class TaskController {
// Dieser Marker allein ersetzt nicht @Valid am Methodenparameter
}
Das Problem ist nicht, dass dies an sich „schlecht“ ist. Das Problem ist die Erwartung: Manche denken, dass jetzt jede Operation automatisch den Body validiert. Das stimmt aber nicht. Für @RequestBody in MVC befindet sich der „Startknopf“ normalerweise am Methodenparameter (@Valid), nicht auf der Klasse.
Um nicht alles im Kopf behalten zu müssen, ist ein kleiner Vergleich hilfreich. Das ist kein „Weltgesetz“, sondern ein praktischer Spickzettel speziell für unser REST-Projekt:
| Vergleichsmerkmal | @Valid | @Validated |
|---|---|---|
| Herkunft der Annotation | Jakarta Validation (jakarta.validation.Valid) | Spring (org.springframework.validation .annotation.Validated) |
| Grundidee | „Prüfe dieses Objekt anhand der Constraints“ | „Aktiviere die Spring-Validierung für Komponente/Methoden (und zusätzliche Modi)“ |
| Typischster Anwendungsfall in unserem Alltag | @Valid @RequestBody TaskCreateRequest request | Tritt manchmal auf Controller/Service-Klasse auf, ersetzt aber nicht @Valid auf dem Body |
| Was passiert, wenn man sie einfach auf die Controller-Klasse setzt? | Nichts (man setzt sie dort auch nicht hin) | Allein garantiert keine Body-Validierung |
| Warum nicht „automatisch“ austauschen? | Das ist der Standardweg für den Body | Weil die Rolle breiter ist und eine falsche Anwendung die Illusion erzeugt, dass alles geprüft wird |
Für das normale @RequestBody-Szenario bleibt der Arbeitsweg heute einfach: @Valid @RequestBody. @Validated wird dort wirklich nützlich, wo die Prüfung über den Body hinausgeht und sich auf Methodenparameter, Query-/Path-Argumente oder Method Validation in einer Komponente bezieht. Daher halten wir uns in den create/update-Beispielen unten bewusst an @Valid: Hier brauchen wir genau die Body-Validierung, ohne Vermischung der Modi.
Und noch ein subtiler Punkt: Technisch erkennt Spring MVC sowohl @Valid als auch @Validated am Parameter. Aber gerade weil @Validated in der Spring-Ökosystem eine zusätzliche Bedeutung hat, sollten wir in einem Lernprojekt nicht mischen. Wir wollen, dass der Code eindeutig lesbar ist: @Valid gesehen – verstanden „wir validieren das Request-DTO“.
4. Pattern: @Valid @RequestBody in create/update
Jetzt machen wir das, was in einem Lernprojekt am meisten geschätzt wird: ein wiederholbares Pattern festlegen, das man später ohne Überraschungen kopieren kann. Es ist wichtig, dass create und update einheitlich sind, denn die API wächst, und das Chaos wächst schneller als die API (Chaos hat vermutlich ein besseres CI/CD).
Hier kommt es auf die Eingangsgrenze an: Wo steht @Valid, gelangt die Anfrage zum Service, und funktioniert das bei create und update gleich? Die Form der Antwort ist in diesem Schritt zweitrangig.
Hier ist die „kanonische“ Form eines create-Endpunkts: Das Request-DTO wird am Eingang validiert, der Controller bleibt dünn, und der Service erhält ein anständiges Objekt, kein „Müllpaket“.
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping
public ResponseEntity<Void> create(
@Valid @RequestBody TaskCreateRequest request // Validieren der Eingabedaten direkt an der API-Grenze
) {
// Wenn request ungültig ist, gibt Spring einen Fehler zurück, bevor wir zum Service kommen
taskService.create(request);
return ResponseEntity.status(201).build();
}
Und genau derselbe Ansatz für update via PUT:
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PutMapping("/{taskId}")
public ResponseEntity<Void> update(
@PathVariable String taskId,
@Valid @RequestBody TaskPutRequest request // Derselbe "Startknopf" für die Prüfung wie bei create
) {
taskService.update(taskId, request);
return ResponseEntity.ok().build();
}
Beachten Sie eines: @Valid in diesen Beispielen ist keine Geschäftslogik und kein „dicker Controller“. Es ist lediglich eine Deklaration: „An der API-Grenze lassen wir nichts durch, was nicht dem Vertrag entspricht.“ Das war’s. Der Controller kennt nicht die einzelnen Regeln (die liegen auf dem DTO) und führt keine manuelle Prüfung durch.
Und nun ein Antipattern, das äußerlich „fast genauso“ aussieht, aber sinngemäß etwas ganz anderes ist. Ein DTO mit Constraints kann wunderbar existieren, aber ohne @Valid gelangt es ohne automatische Prüfung in die Methode:
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PutMapping("/{taskId}")
public void update(@PathVariable String taskId, @RequestBody TaskPutRequest request) {
// Kein @Valid → das "Drehkreuz ist aus": request gelangt ohne automatische Prüfung der Constraints in den Service
taskService.update(taskId, request);
}
Eine solche Signatur wird die Anwendung nicht sofort „zerstören“. Sie zerstört das Gefährlichste: die Vorhersagbarkeit des Vertrags. Sie werden tiefer im Code „seltsame Daten“ erhalten und dann heldenhaft mit manuellen if-Abfragen die Situation retten. Das ist, als ob Sie keine Eingangstür einbauen: Im Winter kann man auch leben, aber man lebt mit Charakter.
5. Checkliste: Wenn die Validierung nicht funktioniert
Wenn es scheint, dass @NotBlank „ignoriert“ wird, ist es leicht, eine Verschwörung zu vermuten: Spring, Jackson, IDE, die Planeten stehen falsch. In der Praxis sind es meist einige konkrete und ziemlich prosaische Ursachen. Das Erfreulichste: Fast alle sind im Code innerhalb einer Minute zu diagnostizieren, wenn man an der richtigen Stelle sucht.
Schauen Sie zuerst auf den Methodenparameter des Controllers. Ist @Valid genau auf dem Parameter, der validiert werden soll? In Projekten dieser Art sind 90 % der Fälle einfach eine vergessene Annotation. Das DTO ist korrekt annotiert, die Abhängigkeit ist richtig eingebunden, aber der „Startknopf“ wurde nicht gedrückt.
Wenn @Valid vorhanden ist, der nächste Schritt – prüfen Sie die Imports. In Boot 4 ist alles in jakarta.*. Wenn Sie versehentlich javax.* importiert haben, kann die IDE das nicht sofort hervorheben, und Sie werden versuchen zu verstehen, warum ein Paket „nicht gefunden“ wird und ein anderes „irgendwie komisch“ ist. Besonders lustig wird es, wenn ein Teil der Annotationen aus jakarta stammt und ein Teil zufällig aus einem alten Beispiel – ein Frankenstein, der selbst den Compiler erschreckt.
Als nächstes sollten Sie sich ehrlich fragen: Gibt es überhaupt etwas zu validieren? Das heißt, gibt es Constraints auf dem DTO? Manchmal kommt folgende Situation vor: Wir haben @Valid gesetzt, erwarten „Magie“, aber das DTO enthält überhaupt keine @Not.../@Size usw. In diesem Fall prüft der Validator ehrlich „nichts“ und sagt „alles gut“. Das ist kein Bug – es ist die wörtliche Ausführung Ihrer Aufforderung.
Und schließlich: Wenn Sie @Validated auf der Controller-Klasse sehen, gehen Sie nicht davon aus, dass dies automatisch die Body-Validierung startet. Für @RequestBody ist der entscheidende Punkt der Methodenparameter. @Validated auf der Klasse kann in anderen Szenarien nützlich sein, aber im Rahmen des heutigen grundlegenden Body-Flusses sollten Sie es eher als „nicht zu dieser Aufgabe gehörig“ betrachten.
6. Kurze schematische Darstellung für @Valid @RequestBody
Für @RequestBody ist der Ablauf bereits bekannt: Spring liest zuerst das JSON in ein DTO, dann startet @Valid die Bean Validation, und erst danach gelangt die Kontrolle in die Controller-Methode. Wenn der Body nicht gelesen werden konnte, liegt ein Conversion-Fehler vor, und die Constraints kommen nicht zum Zug. Wenn das DTO erstellt wurde, aber gegen Constraints verstößt, wird die Anfrage an der Validierung gestoppt – genau deshalb muss der Service nicht erneut einen leeren title oder ähnliche Dinge prüfen.
7. Typische Fehler bei der Validierung
In dieser Phase passieren leicht einige typische Fehler, weil alles äußerlich „fast richtig“ aussieht: Das DTO ist annotiert, die Annotationen sind gesetzt, der Controller kompiliert. Die Fehler zeigen sich nicht als roter Bildschirm des Todes, sondern als „seltsame Daten“ im Service oder als unerwartetes Fehlen von Prüfungen. Lassen Sie uns die häufigsten Fallstricke durchgehen, damit Sie wenigstens einmal darauf treten – und nicht in jedem Projekt.
Fehler Nr. 1: Constraints auf dem DTO sind vorhanden, aber @Valid auf @RequestBody wurde vergessen.
Das ist der häufigste Fall. Visuell sieht der Code „fast richtig“ aus, aber tatsächlich haben Sie die automatische Prüfung an der Grenze nicht aktiviert. Eine gute Gewohnheit ist es, die Signatur der Controller-Methode genauso aufmerksam zu betrachten wie den URI und den Statuscode: Sie ist ebenfalls Teil des Vertrags.
Fehler Nr. 2: javax.validation.* in einem Spring Boot 4-Projekt importiert.
Alte Artikel, alte StackOverflow-Antworten und alte Gewohnheiten greifen oft zu javax zurück. In unserem Baseline muss alles jakarta.validation.* sein. Wenn Sie „package javax.validation does not exist“ sehen – das ist nicht, weil Spring „kaputt“ ist, sondern weil Sie versehentlich einen Gruß aus dem Jahr 2018 mitgebracht haben.
Fehler Nr. 3: @Validated auf den Controller gesetzt und erwartet, dass der Body von selbst validiert wird.
@Validated ist eine nützliche Annotation, ersetzt aber nicht @Valid am Parameter von @RequestBody im Basisszenario. Wenn Sie möchten, dass das Request-DTO geprüft wird, setzen Sie den „Startknopf“ dorthin, wo das DTO in die Methode eintritt.
Fehler Nr. 4: Man versucht, die Validierungsregeln im Controller zu halten und betrachtet @Valid als „Option“.
Wenn die Regeln nicht auf dem DTO, sondern in manuellen if-Abfragen beschrieben sind, verlieren Sie den Hauptvorteil: Der Vertrag wird implizit. Das DTO wird nicht mehr zum „Vertragsdokument“, sondern zu einem einfachen Container. Die Validierung verteilt sich im Projekt, und Sie landen wieder dort, wo Sie nicht hinwollten.
Fehler Nr. 5: Man erwartet, dass @Valid alles auf einmal prüft – „leer“, „Format“ und „Geschäftslogik“.
@Valid dient der Prüfung des Eingangs anhand von Constraints, nicht fachspezifischen Konflikten und nicht dem „Systemzustand“. Wenn Sie etwas prüfen müssen, das Kenntnis der aktuellen Anwendungsdaten erfordert, gehört das in eine andere Schicht. Heute ziehen wir nur diese Grenze: Versuchen Sie nicht, die ganze Welt in eine Annotation zu packen.
GO TO FULL VERSION