1. Aktuell: JSON ist immer noch String
Betrachtet man die aktuelle Version von ReadLater Starter nach dem Transport-Refactoring, ergibt sich ein ziemlich ehrliches Bild: Anfragen werden gesendet, Antworten kommen zurück, der Status ist prüfbar, JSON kann ausgegeben werden. Aber aus Sicht der Anwendung sehen die Daten immer noch aus wie ein „Beutel voller Zeichen“. Das ist am Anfang in Ordnung – genauso beginnen die meisten Leute. Das Problem beginnt, wenn Sie JSON nicht nur ansehen, sondern verwenden möchten.
In der Realität liefert uns die Transportschicht ungefähr Folgendes (vereinfacht):
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// HTTP-Client – Low-Level-Transport: Er weiß noch nichts über „Bücher“, „Autoren“ usw.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
// Wichtig: Hier operieren wir nur mit URL und HTTP-Methode – das ist noch kein „Anwendungsmodell“
.uri(URI.create("https://catalog.example/api/search?q=clean%20code"))
.GET()
.build();
// Das Ergebnis ist vorerst nur Status + Zeichenkette (rohes JSON)
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Transport-Ergebnis prüfen
System.out.println(response.statusCode()); // 200
// Und den Body ausgeben wie er ist (das sind noch keine „Anwendungsdaten“, sondern eine Hülle)
System.out.println(response.body()); // {"docs":[...],"numFound":123}
Aus Sicht von „wir können über HTTP kommunizieren“ ist das ein Sieg. Aber aus Sicht von „wir bauen eine Anwendung“ taucht sofort eine alltägliche Frage auf: Wie bekomme ich jetzt eine Liste von Büchern, deren Titel und Autoren, und zwar so, dass der Code lesbar ist und nicht beim ersten Huster des Anbieters zerbricht?
Man könnte natürlich weiter im Modus „na, ich habe irgendwo gesehen, dass es ein Feld docs gibt, das werde ich jetzt mit substring rausholen“ leben. Und genau hier beginnt das, was Programmierer eine „Erfolgsgeschichte im Horrorgenre“ nennen.
2. Roher JSON-String: „Sand im Getriebe“
Mit JSON wie mit einem String zu arbeiten ist, als würde man versuchen, Borschtsch zu kochen, ohne ein Messer zu haben, und stattdessen eine Bankkarte zu benutzen. Technisch machbar, aber nach der dritten Möhre beginnt man über den Sinn des Lebens nachzudenken, nicht über das Rezept. String-Methoden (contains, indexOf, substring) verstehen die Struktur von JSON nicht: Sie sehen nur Zeichen, keine Felder, Arrays und Verschachtelungen.
Das harmloseste Beispiel sieht meist so aus:
// Rohes JSON als String (wir verstehen die Struktur nicht, sehen nur Zeichen)
String rawJson = """
{"title":"Clean Code","author":"Robert C. Martin"}
""";
// Prüfung auf der Ebene „die Zeichenkette enthält eine Teilzeichenkette“
boolean hasTitle = rawJson.contains("\"title\"");
System.out.println(hasTitle); // true
Das ist zunächst nicht peinlich. Aber es ist eine Prüfung auf der Ebene „der Text enthält das Wort title“. Sie sagt uns nicht, welchen Wert title hat, garantiert nicht, dass es genau das Feld ist, das wir brauchen, und kann leicht durch irgendetwas getäuscht werden.
Der nächste Schritt eines Anfängers ist, den Wert manuell zu extrahieren:
String rawJson = """
{"title":"Clean Code","author":"Robert C. Martin"}
""";
// Suche den Beginn des Feldwerts title (zerbrechlich: abhängig von Format, Leerzeichen, Feldreihenfolge)
int start = rawJson.indexOf("\"title\":\"") + "\"title\":\"".length();
// Suche das nächste Anführungszeichen als „Ende des Werts“ (bricht bei Escape und Anführungszeichen im Inneren)
int end = rawJson.indexOf("\"", start);
// Schneide ein Stück der Zeichenkette aus: „Parsing“, das eigentlich kein JSON parst
String title = rawJson.substring(start, end);
System.out.println(title); // Clean Code
Solange das JSON perfekt flach ist, keine Escapes, keine Leerzeichen, keine Verschachtelung und keine sich wiederholenden Schlüssel enthält, funktioniert es sogar. Aber echtes externes JSON zerbricht diesen „Ansatz“ schnell aus vielen Gründen.
Wenn der Anbieter Leerzeichen hinzufügt oder die Reihenfolge der Felder ändert, kann die String-Logik plötzlich das benötigte Stück nicht mehr finden. Wenn im Wert Anführungszeichen, Zeilenumbrüche oder Sonderzeichen auftauchen, beginnt Ihr indexOf an der falschen Stelle zu „schneiden“. Wenn title in einem Array von Objekten und nicht auf der Wurzel-Ebene liegt – dann schreiben Sie einen Array-Parser. Spoiler: Sie werden versehentlich Ihren eigenen „Jackson“ schreiben, nur schlechter, langsamer und mit Fehlern. Das ist ein normaler Weg – aber besser, man durchläuft ihn theoretisch, nicht in der Produktion und nicht im Kursprojekt.
Und das Unangenehmste: String-Parsing macht Fehler nicht offensichtlich. Es stürzt oft nicht schön ab, sondern extrahiert Daten „ein bisschen falsch“, und Sie überlegen dann lange, warum der Autor eines Buches plötzlich "Martin","status":"PLANNED"... ist (ja, das passiert, wenn Sie versehentlich mit substring an die falsche Stelle geraten).
3. Mapping-Schicht: JSON → DTO
Wenn eine Anwendung wächst, ist es sehr nützlich, klare Verantwortungsgrenzen zu haben. Die Transportschicht ist bereits zu einer solchen Grenze geworden: Sie kommuniziert über HTTP und gibt Status + rohen Body zurück. Jetzt brauchen wir den nächsten „Adapter“: eine Schicht, die den JSON-String in ein Java-Objekt mit verständlicher Form umwandelt. Das ist die JSON-Mapping-Schicht – ein separater Schritt zwischen response.body() und Ihrem Anwendungscode.
Wenn wir die Idee schematisch darstellen, ergibt sich etwa Folgendes:
flowchart LR
%% Idee: Der „JSON-String“ lebt an der Grenze, im Inneren der Anwendung sind Typen und Felder
A["HTTP response.body() String"] --> B["JSON-Mapping-Schicht Jackson"]
B --> C["Typisiertes DTO Java record"]
C --> D["Weiter arbeitet der Code mit Feldern und Typen, nicht mit Strings"]
Aus praktischer Sicht bietet die Mapping-Schicht drei sehr handfeste Vorteile.
Erster Vorteil: Sie beginnen, mit Daten über Felder und Typen zu arbeiten, nicht über „magische“ Teile eines Strings. book.title() liest sich wie menschlicher Text. substring(17, 42) liest sich wie „ich bin müde, Chef“.
Zweiter Vorteil: Fehler werden lokal. Wenn JSON nicht der erwarteten Form entspricht, manifestiert sich das Problem genau an der Grenze JSON → DTO, nicht 15 Zeilen später, wenn Sie irgendwo versehentlich null erhalten und an einer anderen Stelle abstürzen.
Dritter Vorteil: Sie stärken die Disziplin: Rohes JSON bleibt an der Grenze Transport/Mapping und sickert nicht in die Anwendung hinein. Das ist eine sehr wichtige Gewohnheit vor Spring: Später automatisiert das Framework vieles, aber die Idee der Grenzen verschwindet dadurch nicht.
4. Jackson 3: Übersetzer JSON ↔ Java
Um das Rad nicht aus Nudeln und Knete neu zu erfinden, nehmen wir eine Bibliothek, die professionell die Übersetzung zwischen JSON und Java-Objekten übernimmt. In unserem Kurs ist das Jackson 3 – und wir verwenden es als einheitliche Baseline. Die zentrale Klasse für die tägliche Arbeit ist hier ObjectMapper, und in Jackson 3 befindet sie sich im Paket tools.jackson.databind.
Die grundlegendste Magie (eigentlich keine Magie, sondern ein normales Werkzeug) sieht so aus: Wir deklarieren ein DTO und bitten dann Jackson, JSON in dieses DTO zu lesen.
import tools.jackson.databind.ObjectMapper;
public class JacksonMiniDemo {
// DTO (Vertrag): In dieser Form möchten wir die Daten innerhalb der Anwendung sehen
record BookDto(String title, String author) {}
public static void main(String[] args) throws Exception {
// Rohes JSON kam als String (z. B. aus response.body())
String rawJson = """
{"title":"Clean Code","author":"Robert C. Martin"}
""";
// ObjectMapper – unser „Übersetzer“ zwischen JSON und Java-Typen
ObjectMapper mapper = new ObjectMapper();
// Deserialisierung: JSON -> typisiertes DTO
BookDto book = mapper.readValue(rawJson, BookDto.class);
// Jetzt arbeiten wir mit Feldern, nicht mit Teilzeichenketten
System.out.println(book.title()); // Clean Code
}
}
Beachten Sie den wichtigen psychologischen Moment: Im Code gibt es kein „Suchen nach der Teilzeichenkette \"title\":\"“ mehr. Wir sagen einfach: „Hier ist der Vertrag, hier ist das DTO, übersetze es.“
Die umgekehrte Operation gibt es auch: Derselbe ObjectMapper kann JSON aus einem Java-Objekt erzeugen. Aber hier ist für uns das Lesen wichtiger. Sobald response.body() schnell in ein DTO verwandelt wird, hört die Anwendung auf, in der Welt von substring zu leben, und beginnt, mit Typen zu arbeiten.
Im Moment reicht es, das Prinzip zu verstehen: ObjectMapper ist die Grenze zwischen String-JSON und typisierten Anwendungsdaten. Eine separate technische Gewohnheit besteht darin, solches Mapping an einer Stelle zusammenzuhalten und nicht über den Code zu verstreuen.
5. Baseline: Jackson 3 ohne Jackson 2
Im Internet finden Sie Tonnen von Beispielen „wie man JSON in Java parst“. Das Problem ist, dass ein erheblicher Teil dieser Beispiele für Jackson 2 geschrieben ist, und Jackson 3 ist nicht einfach die „nächste Version“ – dort haben sich tatsächlich wichtige „Einstiegspunkte“ geändert: Maven-Koordinaten und Java-Pakete. In Jackson 3 sind die Hauptpakete in den Namensraum tools.jackson.* umgezogen, damit Jackson 2 und Jackson 3 koexistieren können, ohne sich auf dem Classpath zu beißen.
Hier ist eine kurze Tabelle, die vor 80 % der Anfängerschmerzen bewahrt:
| Was Sie im Beispiel sehen | Wahrscheinlich ist das | Was tun wir in unserem Kurs |
|---|---|---|
| implementation("com.fasterxml.jackson.core:jackson-databind:...") | Jackson 2 | Nicht 1:1 kopieren |
| implementation("tools.jackson.core:jackson-databind:3.0.4") | Jackson 3 | Das ist unsere Baseline |
| import com.fasterxml.jackson.databind.ObjectMapper; | Jackson 2 | Das würde bei uns nicht kompilieren |
| import tools.jackson.databind.ObjectMapper; | Jackson 3 | So ist es richtig |
| import com.fasterxml.jackson.annotation.JsonProperty; | Jackson-Annotationen | Das ist normal sogar in Jackson 3 (Annotationen verblieben in der alten groupId) |
Und hier lauert eine tückische Falle. Wenn ein Student einen Importfehler sieht, macht er oft den „logischen“ Schritt: Er fügt eine weitere Abhängigkeit hinzu, um den Import zu „reparieren“. Zum Beispiel ist im Projekt bereits Jackson 3 vorhanden, aber Sie haben Jackson 2 hinzugefügt, „weil der Artikel das so gesagt hat“. Das Ergebnis sind zwei verschiedene Bibliotheken, die ungefähr dasselbe tun, aber in unterschiedlichen Paketen leben. Ja, genau deshalb ist Jackson 3 in tools.jackson.* umgezogen: damit Koexistenz möglich ist.
Nur: Koexistenz ist eine Möglichkeit für große Projekte, die schrittweise migrieren. Für ein Studienprojekt wird das in der Regel zum Chaos: Sie fangen an zu verwechseln, welchen ObjectMapper Sie verwenden, welche Exceptions abzufangen sind, welche Abhängigkeiten wirklich nötig sind. Deshalb ist die Kursregel einfach und ein kleines bisschen pedantisch (also nützlich): Wir halten eine einzige Baseline Jackson 3 und machen im Projekt keinen „Versionen-Zoo“.
Ein weiteres Detail, das wie eine Kleinigkeit aussieht, aber ständig das Leben schwer macht: In Jackson 3 hängt jackson-databind tatsächlich von jackson-annotations ab, und in der POM gibt es eine direkte Erläuterung, dass die Annotationen in der groupId von Jackson 2 (com.fasterxml.jackson.core) verbleiben. Das heißt, die Kombination tools.jackson.databind.ObjectMapper + com.fasterxml.jackson.annotation.JsonProperty ist kein Fehler, sondern ein normaler Teil des Konzepts von Jackson 3.
6. ReadLater Starter: Von response.body() zu DTO
Um Jackson nicht als „ein weiteres Thema im luftleeren Raum“ zu betrachten, lassen Sie uns alles auf unser Projekt beziehen. Wir haben einen Befehl der Art catalog search <query>, der einen HTTP-Aufruf an einen externen Katalog durchführt. Die Transportschicht gibt uns Status und JSON-String zurück. Der nächste Schritt ist nicht „den String hübscher auszugeben“, sondern ihn in ein DTO zu verwandeln, denn genau das DTO wird später normalisiert und dem Benutzer angezeigt.
Vereinfacht gesagt sollte der Gedankenfluss im Code so aussehen:
import tools.jackson.databind.ObjectMapper;
import java.net.http.HttpResponse;
public class CatalogFlowSketch {
// DTO der erfolgreichen Antwort des Anbieters (in Wirklichkeit wird es mehr Felder geben)
record ProviderSearchResponseDto(int numFound) {}
public ProviderSearchResponseDto parse(HttpResponse<String> response) throws Exception {
// Zuerst – Transportprüfung: Status, Fehler, Retries usw.
if (response.statusCode() != 200) {
// An dieser Stelle dokumentieren wir explizit, dass das Problem nicht im JSON-Mapping liegt, sondern in der Antwort des Anbieters
throw new IllegalStateException("Provider returned status " + response.statusCode());
}
// Dann – Mapping: JSON-String in einen Typ umwandeln
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(response.body(), ProviderSearchResponseDto.class);
}
}
Wichtig ist hier die Grenze selbst: Nach der Statusprüfung sollte JSON schnell in ein DTO umgewandelt werden und nicht als String durch das Projekt reisen. Eine separate technische Gewohnheit ist es, ObjectMapper nicht an jeder Stelle neu zu erstellen und die Mapping-Logik nicht über den Code zu verstreuen. Aber selbst in dieser kurzen Skizze ist bereits das Wesentliche zu sehen: Transport-Erfolg und JSON-Mapping sind zwei verschiedene Schritte, und man sollte sie nicht verwechseln.
7. Typische Fehler
Am Anfang der Arbeit mit Jackson passieren fast alle Fehler nicht, weil „Jackson kompliziert ist“, sondern weil das Gehirn aus Gewohnheit dazu neigt, zurück in die Welt der Strings und „schnellen Lösungen“ gezogen zu werden. Das ist normal: Sie lernen. Die Hauptsache ist, die Gewohnheit rechtzeitig am Handgelenk zu packen und sanft, aber bestimmt zurück in die Zivilisation zu führen.
Fehler Nr. 1: JSON weiterhin mit String-Methoden parsen, „weil es schneller ist“.
Das beginnt meist mit contains, dann kommt substring, dann ein „kleiner regulärer Ausdruck“, und plötzlich schreiben Sie die Behandlung von Escape-Anführungszeichen und merken, dass Sie gerade begonnen haben, eine Bibliothek auf Jackson-Niveau zu entwickeln – aber ohne Gehalt. Der richtige Schritt an diesem Tiefpunkt ist, den String als Transportverpackung zu betrachten, die in ein DTO entpackt und dann weggeworfen werden muss.
Fehler Nr. 2: Versuchen, body zu mappen, bevor statusCode geprüft wird.
ObjectMapper muss nicht „erraten“, was Sie erhalten haben. Wenn der Anbieter 404 oder 500 mit einem anderen JSON zurückgibt, und Sie versuchen, dies als erfolgreiches DTO zu lesen, erhalten Sie einen Deserialisierungsfehler an einer Stelle, wo eigentlich ein Problem auf Transportebene vorliegt. Die Disziplin ist einfach: Zuerst die Status-Verzweigung, dann JSON → DTO.
Fehler Nr. 3: Jackson-2-Beispiele kopieren und durch Hinzufügen neuer Abhängigkeiten „reparieren“.
Wenn in einem Artikel ObjectMapper aus com.fasterxml.jackson.databind importiert wird, und Sie haben in Ihrem Projekt Jackson 3, dann müssen Sie die Situation nicht durch Hinzufügen von Jackson 2 retten. Unsere Baseline ist Jackson 3, und ObjectMapper sollte aus tools.jackson.databind kommen. Annotationen können dabei weiterhin com.fasterxml.jackson.annotation.* sein – das ist zu erwarten.
Fehler Nr. 4: Rohes JSON „für alle Fälle“ durch mehrere Schichten schleppen.
Manchmal scheint es nützlich, den String „für später“ aufzubewahren: vielleicht braucht man ihn ja noch. In der Praxis führt das dazu, dass die Hälfte des Codes vom Format des Anbieters abhängt und jede Änderung des externen Vertrags sich durch das gesamte Projekt zieht. Viel entspannter ist es, rohes JSON nur an der Grenze Transport/Mapping zu halten und nach innen DTOs zu übergeben.
GO TO FULL VERSION