CodeGym /Kurse /Spring REST & MVC /Wie @RequestBody funktioniert

Wie @RequestBody funktioniert

Spring REST & MVC
Level 7, Lektion 0
Verfügbar

1. Request Body vs. Pfad und Query

Ehrlich gesagt, der Request Body ist keine “Spring-Besonderheit”. Es ist die normale, kanonische Methode, um viele Daten auf einmal in einer HTTP-Anfrage zu übermitteln. Besonders wenn Sie eine Ressource erstellen oder ändern: Da gibt es meist nicht ein Feld, sondern einen ganzen Satz. Und je erwachsener die Anwendung wird, desto mehr spüren Sie das (und desto weniger Lust haben Sie, auf eine URL mit einer halben Bildschirmbreite zu schauen).

Stellen Sie sich vor, wir möchten eine Aufgabe in der Task Tracker API erstellen. Eine Aufgabe hat mindestens title und description. Schon zwei Felder. Plus kann es einen Bearbeiter, eine Frist, Tags geben … Wenn Sie all das über Query-Parameter übergeben, erhalten Sie so etwas wie einen “erzwungenen Brief in Kleinschrift auf dem Rand”:

# Beispiel “wie es geht, aber besser nicht”: Daten zur Ressourcenerstellung in Query
POST /api/v1/tasks?title=Fix%20login&description=...&assigneeName=...&dueDate=...

Technisch können Sie das so machen, aber der Vertrag wird schwerer lesbar und weniger robust. Query-Parameter sind gut für Filter, Paginierung und “Modi” einer Liste, nicht für die Übergabe des “neuen Zustands einer Ressource”.

Es ist hilfreich, sich folgendes Bild im Kopf zu behalten (ohne es zum Dogma zu erheben). Es ist nicht die “einzige Wahrheit”, sondern ein praktischer Orientierungspunkt, der beim Entwurf einer vorhersagbaren API hilft:

Wohin legen wir Daten Wie sieht das aus Was bedeutet das Beispiel in der Task Tracker API
Pfad (/tasks/{id}) Teil der URI „wen/was adressiere ich“ GET /api/v1/tasks/{taskId}
Query (?status=...) Parameter nach ? „wie möchte ich filtern/anzeigen“ GET /api/v1/tasks?status=TODO
Header Content-Type, Accept usw. „in welchem Format kommunizieren wir“ Content-Type: application/json
Body JSON (oder anderes) „welche Daten übermittle ich für die Operation“ POST /api/v1/tasks + JSON mit Aufgabenfeldern

Der Request Body ist wie ein Paket. Der Pfad ist die Lieferadresse, Query sind die Vermerke „nach 18:00 liefern“ und „nicht an der Gegensprechanlage klingeln“. Wenn Sie versuchen, das Paket selbst in die Vermerke zu stecken, wird der Kurier sich natürlich wundern.

2. Die Rolle von @RequestBody in Spring MVC

Wenn Sie @RequestBody zum ersten Mal sehen, denken Sie sehr leicht: „Oh, das ist eine Annotation, die einfach ein Objekt aus JSON macht.“ Und ja, vom Gefühl her passiert genau das. Aber der wichtige Punkt ist: Es ist keine „Magie“, sondern Teil eines klaren Vertrags zwischen Ihnen und Spring MVC: Sie sagen explizit, woher der Methodenparameterwert kommen soll.

Die Idee ist einfach: In einer Controllermethode möchten Sie nicht einen JSON-String, sondern ein fertiges Java-Objekt erhalten, mit dem Sie bequem arbeiten können. Anstatt manuell einen Bytestrom zu lesen, einen String zu sammeln, JSON zu parsen und dann manuell Felder zu extrahieren, beschreiben Sie die Form der Eingabedaten mit einer Java-Klasse – und Spring führt die Bindung automatisch durch.

Minimal sieht das so aus:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController // Wir sagen Spring: Das ist ein Controller, der Antworten direkt zurückgibt (normalerweise JSON/Text)
public class DemoController {

    @PostMapping("/demo") // Route, die POST-Anfragen akzeptiert
    public String demo(@RequestBody DemoRequest request) {
        // @RequestBody: Nimm den HTTP-Anfragekörper und mappe ihn auf DemoRequest (über den JSON-Konverter)
        return "title=" + request.getTitle(); // Zum Beispiel: title=Fix login
    }
}

Hier bedeutet @RequestBody wörtlich: „Spring, nimm den HTTP-Anfragekörper und versuche, ihn in ein DemoRequest zu verwandeln.“

Damit das überhaupt möglich ist, sendet der Client normalerweise den Header Content-Type: application/json, damit der Server nicht rätseln muss, was genau im Body steckt. Wir haben bereits Content-Type als „Formatpass“ besprochen. Ohne ihn kann der Server anfangen zu zweifeln und mit einem Fehler antworten – aber jetzt ist es wichtiger, das Prinzip zu verstehen: @RequestBody verbindet Body -> Objekt.

Ein weiterer wichtiger Punkt, den Anfänger oft übersehen: Der Request Body ist ein einziger Datenstrom. Daher kann in einer Controllermethode normalerweise nur ein Parameter mit @RequestBody vorhanden sein. Man kann nicht „ein JSON in zwei unabhängige Körper aufteilen“. Technisch ist das wie der Versuch, ein Blatt Papier zweimal zu lesen, nachdem man es im Kamin verbrannt hat. Klingt schön, aber es gibt nichts mehr zu lesen.

3. Klasse für den JSON-Request-Body

Wenn Sie einen Request Body akzeptieren, müssen Sie Spring MVC eine verständliche „Form“ geben – eine Java-Klasse, die die erwartete JSON-Struktur beschreibt. Das ist der Beginn einer Vertragsdisziplin: Sie legen direkt im Code fest, welche Felder Sie erwarten, wie sie heißen und von welchem Typ sie sind. Der Client kann jetzt nicht mehr „irgendetwas“ senden und hoffen, dass der Server „irgendwie zurechtkommt“.

In unserem Projekt wird dies eine Anfrage zum Erstellen einer Aufgabe sein. Erstellen wir die Klasse TaskCreateRequest. Entsprechend der Projektstruktur ist das Paket api.dto.request logisch, da es sich um ein Modell für den Eingang an der API-Grenze handelt.

package com.example.tasktracker.api.dto.request;

public class TaskCreateRequest {

    // Felder, die wir im JSON vom Client erwarten
    private String title;
    private String description;

    // Getter wird benötigt, damit der Controller/Service den Wert lesen kann
    public String getTitle() { return title; }

    // Setter wird benötigt, damit der JSON-Mapper (z.B. Jackson) den Wert in das Objekt schreiben kann
    public void setTitle(String title) { this.title = title; }

    public String getDescription() { return description; }

    public void setDescription(String description) { this.description = description; }
}

Warum brauchen wir hier Getter/Setter? Weil Spring (genauer gesagt das JSON-Mapping im Web-Stack) normalerweise im JavaBean-Stil arbeitet: Es kann ein Objekt erstellen und seine Eigenschaften über Setter befüllen. Das ist nicht der einzige mögliche Stil in Java, aber für Anfänger ist er am verständlichsten und am weitesten verbreitet.

Jetzt kann der Client ein JSON etwa in dieser Form senden:

# Aufgabe erstellen: Daten gehen in den Body, das Format beschreiben wir mit Content-Type
POST /api/v1/tasks
Content-Type: application/json

{
  "title": "Fix login",
  "description": "Users cannot login after password reset"
}

Und wenn alles gut läuft, erhält die Controllermethode ein TaskCreateRequest-Objekt, dessen:

// Diese Werte werden aus den JSON-Feldern "title" und "description" übernommen
request.getTitle();       // "Fix login"
request.getDescription(); // "Users cannot login after password reset"

Hier gibt es eine sehr praktische Zuordnungslogik: Der JSON-Key "title" landetet in der Java-Eigenschaft title, weil es setTitle(...) gibt. Der JSON-Key "description" landet in description, weil es setDescription(...) gibt. Sie müssen sich das nicht als „Regel aus dem Lehrbuch“ merken – es reicht zu wissen, dass die Namen der JSON-Keys mit den Namen der Felder/Eigenschaften übereinstimmen sollten, die Sie erwarten.

Ein wichtiger Hinweis für Anfänger, der oft im unerwartetsten Moment auftaucht. Wenn Sie ein Feld nicht als String, sondern z.B. als int deklarieren, und der Client sendet es nicht, wird Java trotzdem den Standardwert 0 setzen. Sie können dann nicht mehr unterscheiden, ob der Client 0 gesendet hat oder gar nichts. Daher verwendet man für optionale Zahlen/Flags in Request-Modellen eher Integer, Long, Boolean statt int/long/boolean. Für unsere title/description ist das irrelevant, weil sie Strings sind, aber die Gewohnheit sollte man sich gleich aneignen – später werden Sie sich selbst (und Ihren zukünftigen Bug-Reports) dafür danken.

Ein weiterer Gedanke: Die Request-Klasse sollte nur das beschreiben, was tatsächlich vom Client kommt. Wenn Sie versuchen, die „vollständige Aufgabe“ einschließlich id, createdAt usw. zu akzeptieren, erlauben Sie dem Client damit, über Serverfelder zu „bestimmen“. In unserem Projekt generiert der Server die Aufgaben-ID, daher gibt es in TaskCreateRequest kein id. Das ist eine sehr gesunde Gewohnheit für eine API: Der Client sendet das, was er festlegen darf, und der Server das, was er selbst kontrollieren muss.

4. @RequestBody + @PathVariable

Wenn Sie eine Ressource erstellen, gibt es noch nichts, was die konkrete Entität adressiert – ihre id existiert vor der Erstellung nicht. Sobald wir jedoch über Aktualisierung sprechen, wird es interessanter: Wir müssen gleichzeitig angeben, welche Ressource wir ändern und mit welchen Daten wir sie ändern. Und genau hier funktioniert die Kombination aus Pfad und Body wunderbar.

Der Pfad bleibt die „Adresse“ der Ressource, und der Body wird zum „neuen Inhalt“. Das ist der realen Welt sehr ähnlich: Sie können einen Brief an eine Adresse (Pfad) senden, und was im Umschlag ist (Body), sind die Operationsdaten.

Beispiel für eine Methodensignatur zur „vollständigen Ersetzung“ (ohne jetzt auf die Semantik von PUT und PATCH einzugehen, uns interessiert nur die Mechanik der Datenübergabe):

import com.example.tasktracker.api.dto.request.TaskUpdateRequest;
import com.example.tasktracker.domain.model.Task;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

public class TaskController {

    @PutMapping("/api/v1/tasks/{taskId}")
    public Task replace(@PathVariable String taskId,
                        @RequestBody TaskUpdateRequest request) {
        // taskId – „wen wir ändern“ (Adresse der Ressource in der URI)
        // request – „womit wir ändern“ (Operationsdaten im Body)
        return null;
    }
}

Beachten Sie, dass taskId hier im Pfad steht und nicht im JSON. Das macht den Vertrag weniger mehrdeutig: Die URI adressiert eindeutig die Ressource, und der Request Body beschreibt eindeutig die Änderungsdaten. Wenn Leute beginnen, die id sowohl dort als auch dort zu duplizieren, endet das fast immer mit der Frage „Was tun, wenn sie unterschiedlich sind?“. Die richtige Antwort lautet meistens: „Nicht duplizieren.“

5. POST /api/v1/tasks mit @RequestBody

Jetzt machen wir einen praktischen Schritt in unserem Projekt: Wir fügen einen Endpunkt zum Erstellen einer Aufgabe hinzu. In dieser Vorlesung ist es wichtig zu verstehen, wie man JSON als Objekt akzeptiert und es an den Service weiterleitet, ohne den Controller zu einem Ort für manuelles Parsen und Logik zu machen.

Beginnen wir mit dem Controller. Angenommen, wir haben bereits einen TaskController mit GET-Methoden aus früheren Tagen und einen TaskService, der für die Anwendungslogik verantwortlich ist. Wir fügen @PostMapping und @RequestBody hinzu:

package com.example.tasktracker.api.controller;

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import com.example.tasktracker.domain.model.Task;
import com.example.tasktracker.domain.service.TaskService;
import org.springframework.web.bind.annotation.*;

@RestController // REST-Controller: Wir geben ein Objekt zurück, das in JSON serialisiert wird
@RequestMapping("/api/v1/tasks") // Basis-Pfad für alle Methoden dieses Controllers
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        // Abhängigkeitsinjektion: Der Controller „erstellt keine Aufgaben“ selbst, er delegiert an den Service
        this.taskService = taskService;
    }

    @PostMapping // POST /api/v1/tasks
    public Task create(@RequestBody TaskCreateRequest request) {
        // request kommt aus dem Body, dann extrahieren wir die benötigten Felder und übergeben sie an den Service
        return taskService.create(request.getTitle(), request.getDescription());
    }
}

Beachten Sie, dass wir den Service mit einfachen Typen (String, String) aufrufen. Das hilft, die Schichtengrenze sauber zu halten: Die Service-Schicht muss nicht wissen, wie genau die HTTP-Anfrage aussah. Sie kennt nur den Sinn der Operation: „Erstelle eine Aufgabe mit diesem Titel und dieser Beschreibung.“

Schauen wir uns nun ein Beispiel für den Service an. Auf dem aktuellen Kursniveau reicht eine einfache In-Memory-Implementierung (ohne Datenbank). Der Service speichert Aufgaben in einer Map, generiert eine id und gibt die erstellte Aufgabe zurück:

package com.example.tasktracker.domain.service;

import com.example.tasktracker.domain.model.Task;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class TaskService {

    // In-Memory-Speicher: lebt nur, solange die Anwendung läuft
    private final Map<String, Task> tasks = new ConcurrentHashMap<>();

    public Task create(String title, String description) {
        // ID serverseitig generieren (der Client sollte dieses Feld nicht verwalten)
        String id = UUID.randomUUID().toString();

        // Domänen-Entität aus den Eingabedaten zusammenbauen
        Task task = new Task(id, title, description);

        // In unserem temporären Speicher ablegen
        tasks.put(id, task);

        // Die erstellte Entität zurückgeben (wird später von Spring serialisiert)
        return task;
    }
}

Und hier ein wichtiger Hinweis: Ich zeige absichtlich ein vereinfachtes Modell von Task mit dem Konstruktor new Task(id, title, description). In Ihrem Projekt kann Task bereits mehr Felder haben (z.B. einen Status), dann wird der Konstruktor anders aussehen – das ist normal. Für uns ist jetzt etwas anderes prinzipiell: Wir akzeptieren Task nicht direkt aus dem Request Body, sondern TaskCreateRequest und erstellen dann Task auf dem Server.

Jetzt können wir testen, ob Spring tatsächlich ein Objekt aus JSON „zusammenbaut“. Der einfachste Weg ist eine .http-Anfrage:

### Aufgabe erstellen
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json

{
  "title": "Fix login",
  "description": "Users cannot login after password reset"
}

Wenn alles richtig gemacht ist, erhalten Sie eine Antwort mit JSON (deren Format von Ihrem Task abhängt):

{
  "id": "b63a3e79-430b-4f1e-8f2b-1f8e8b4a1e6b",
  "title": "Fix login",
  "description": "Users cannot login after password reset"
}

Und das ist das zentrale Ergebnis dieser Vorlesung: Sie haben JSON als Text gesendet und im Java-Code mit einem normalen Objekt gearbeitet, als ob es „schon immer da gewesen wäre“.

6. Vertragsdisziplin für den Request Body

Der Trick mit @RequestBody erscheint einfach, aber er bringt eine sehr wichtige Disziplin in das Projekt: Sie beginnen, die Form des eingehenden JSON als Teil eines externen Vertrags zu betrachten. Das ist nicht „wir werden es irgendwie parsen“, sondern „wir erwarten eine bestimmte Struktur“. Und genau deshalb ist es besser, den Request Body in einem Objekt zu sammeln, anstatt ihn über zehn Parameter zu verteilen.

Eine schlechte, aber häufig anzutreffende Variante sieht so aus: Erstellen einer Ressource über eine Reihe von Parametern (normalerweise Query), weil „es schneller zu schreiben ist“ und „was soll schon passieren“. Am Ende wird der Vertrag weniger klar, und der Request Body bleibt ungenutzt:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

public class BadController {

    @PostMapping("/api/v1/tasks")
    public String create(@RequestParam String title,
                         @RequestParam String description) {
        // Hier kommen die „Erstellungsdaten“ nicht im Body, sondern in Query-Parametern,
        // wodurch der Vertrag aufgebläht und schwerer lesbar wird.
        return "ok";
    }
}

Eine gute Variante ist ein einzelner @RequestBody-Parameter, der die gesamte Eingabestruktur beschreibt. Dann wird JSON selbst zum „Dokument“ und die Java-Klasse zur „Beschreibung des Dokuments“:

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

public class GoodController {

    @PostMapping("/api/v1/tasks")
    public String create(@RequestBody TaskCreateRequest request) {
        // Der gesamte Eingabe-Vertrag ist an einem Ort versammelt: in der Request-Klasse
        return "title=" + request.getTitle();
    }
}

Und noch ein Anti-Chaos-Tipp, der Stunden des Lebens spart: Duplizieren Sie die Ressourcen-ID nicht sowohl im Pfad als auch im Body. Wenn Sie PUT /tasks/{taskId} haben, dann lebt taskId im Pfad. Wenn Sie auch noch eine id im JSON hinzufügen, schaffen Sie selbst die Situation „welche ist nun die richtige“. Und das Backend hat schon genug zu tun, es braucht nicht noch eine zusätzliche Aufgabe.

7. Typische Fehler bei der Arbeit mit @RequestBody

Fehler Nr. 1: Erstellungs-/Aktualisierungsdaten über Query-Parameter übergeben, „weil es einfacher ist“.
So ist es zwar einmal einfacher zu schreiben, aber der Vertrag wird schmutziger und schwerer wartbar. Für Erstellungsoperationen mit vielen Feldern bietet der Request Body mit JSON fast immer eine lesbarere und robustere Schnittstelle.

Fehler Nr. 2: Versuchen, JSON manuell als String zu lesen (HttpServletRequest, getReader()) und dann selbst zu parsen.
Das ist der klassische Weg zu „deinem eigenen Framework“. Sie verschwenden Zeit mit Infrastruktur, die Spring MVC bereits für Sie erledigt, und beginnen, Fehler an der empfindlichsten Stelle zu produzieren – an der Dateneingangsgrenze. Der Controller sollte ein Objekt erhalten und die Arbeit an den Service delegieren, nicht zu einem Mini-Parser werden.

Fehler Nr. 3: Erwarten, dass man zwei @RequestBody-Parameter in einer Methode haben kann.
HTTP hat einen einzigen Body. Er kann sich nicht in zwei unabhängige Objekte „aufspalten“. Wenn Sie denken, Sie brauchen zwei Bodies, dann brauchen Sie ein Objekt mit zwei Feldern oder ein Objekt mit einer verschachtelten Struktur.

Fehler Nr. 4: Nichtübereinstimmung der Namen von JSON-Feldern und Java-Eigenschaften.
Wenn der Client "taskTitle" sendet, Ihre Klasse aber title erwartet, wird das Feld title nicht gefüllt und bleibt null. Das ist keine „Laune von Spring“, sondern der natürliche Preis der Vertragsdisziplin: Der Server erwartet eine bestimmte Struktur. Halten Sie daher JSON-Beispiele bereit und überprüfen Sie, ob die Namen übereinstimmen.

Fehler Nr. 5: Verwendung von primitiven Typen (int, boolean) dort, wo ein Feld optional sein kann.
Wenn das Feld nicht kommt, wird int zu 0 und boolean zu false. Und Sie verlieren die Bedeutung: Wurde es vom Client gesendet oder ist es nur der Standardwert? Verwenden Sie für potenziell fehlende Felder Wrapper (Integer, Boolean) und unterscheiden Sie explizit zwischen „kein Wert“ und „Wert ist null/false“.

1
Aufgabe
Spring REST & MVC, Level 7, Lektion 0
Gesperrt
Notiz aus JSON-Body erstellen
Notiz aus JSON-Body erstellen
1
Aufgabe
Spring REST & MVC, Level 7, Lektion 0
Gesperrt
Buch ersetzen nach ID mit Pfad und Body
Buch ersetzen nach ID mit Pfad und Body
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION