CodeGym /Kurse /Spring REST & MVC /Fehleradresse: path und Indizes

Fehleradresse: path und Indizes

Spring REST & MVC
Level 18, Lektion 1
Verfügbar

1. Fehler ohne Adresse: die Rolle von path

Wenn man darüber nachdenkt, ist ein Validierungsfehler keine „Bestrafung“ des Kunden für eine schlechte Anfrage, sondern ein Hinweis: was genau korrigiert werden muss. Und hier entsteht ein einfaches, aber schmerzhaftes Problem: Ohne Adresse wird der Fehler zu einem Rätsel der Stufe „irgendwo im JSON stimmt etwas nicht“. Bei einem kleinen Payload ist das noch erträglich, aber sobald verschachtelte Strukturen, Listen, Filter und Cross-Field-Regeln auftauchen, verwandeln Sie den Kunden ohne genaue Ortsangabe in einen Archäologen.

Die Antwortform selbst ist bereits vorhanden: oben status, code, summary, errors, und in der Liste der Details befinden sich die konkreten Verstöße. Nun ist die nächste Frage entscheidend – wie benennt man den Fehlerort so, dass der Kunde nicht raten muss, welches Feld oder Listenelement defekt ist.

Path in den Fehlerdetails beantwortet die Frage „wo genau liegt das Problem?“. Er sollte einfach genug sein, damit der Kunde sich darauf stützen kann, und genau genug, um ein bestimmtes Feld in der UI hervorheben, ein verständliches Log schreiben oder dem Benutzer die genaue Ursache anzeigen zu können. Wichtig ist, dass path kein „schöner Text“ ist, sondern ein Teil des Vertrags: Sie haben vereinbart, dass die Daten im Eingang so adressiert werden, und behalten diesen Stil überall bei.

Stellen Sie sich eine Anfrage zum Erstellen einer Aufgabe vor, bei der der Kunde zehn Felder, eine Liste von Tags und noch ein paar verschachtelte Objekte gesendet hat. Wenn Sie nur „title ist erforderlich“ zurückgeben – kann der Kunde es noch verstehen. Aber wenn Sie „es gibt einen Fehler in tags“ zurückgeben – muss der Kunde raten, welches Tag genau defekt ist, besonders wenn es viele sind. Wenn Sie jedoch tags[3] zurückgeben – wird alles offensichtlich: korrigiere das vierte Element der Liste (und ja, Indizes sind ein separates Thema, darauf kommen wir noch zu sprechen).

Um nicht nur zu reden, legen wir nur den Teil des Details fest, der für die Diskussion über die Fehleradresse benötigt wird. Das vollständige kanonische Detail-Objekt ist bei uns breiter, aber jetzt interessiert uns genau path.

// Vereinfachtes Schema des Details nur für die Diskussion über path.
// Das vollständige kanonische Detail-Objekt ist breiter, aber jetzt interessiert uns die Fehleradresse.
record ValidationErrorDetail(
    String source, // woher der Fehler kommt: body/query/path
    String path,   // Adresse der problematischen Stelle in den Eingabedaten
    String message // für den Kunden/Benutzer lesbare Nachricht
) {}

Hier ist path die besagte „Adresse“. In den folgenden Abschnitten werden wir die Grammatik der Adresse festlegen: wie sie für ein einfaches Feld, für ein verschachteltes Objekt, für eine Liste und für Group-Fehler aussieht.

2. Field path: Fehler eines einzelnen Feldes

Der häufigste Fall der Validierung ist eine Regelverletzung bei einem einzelnen Feld: leeres title, zu langes assigneeName, negative size bei Paginierung und so weiter. In solchen Situationen sollte path so auf das konkrete Feld verweisen, dass der Kunde nicht „dazwischen denken“ muss, wovon Sie sprechen. Je weniger Magie, desto besser: Wenn die Anfrage ein Feld title enthält, sollte der Fehlerpfad title sein. Nicht task.title, nicht request.title, nicht field_1_title, sondern genau der Name, den der Kunde im öffentlichen Vertrag sieht.

Betrachten wir Beispiele aus dem Task Tracker API. Für das Body-Szenario mit TaskCreateRequest sieht der einfachste Fall so aus:

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",                // Fehler bezieht sich auf den JSON-Request-Body
    "title",               // konkretes Feld im eingehenden JSON
    "Feld title ist erforderlich" // Text, der dem Benutzer angezeigt werden kann
);

Hier sagt source="body" einfach, dass der Fehler zum JSON-Request-Body gehört, und path="title" zum konkreten Feld. Dieses Feld kann in der Form hervorgehoben werden, in Tests genutzt, Fehlerstatistiken gesammelt werden – und das alles ohne Parsen des Nachrichtentexts.

Nun der Query-Parameter. In GET /api/v1/tasks haben wir zum Beispiel size (Seitengröße). Wenn der Kunde size=200 sendet, und wir erlauben maximal 100, dann können wir den Pfad einfach als size angeben. Es ist nicht nötig, criteria.size zu erfinden, wenn der Kunde kein criteria sieht – er sieht die Query-String.

ValidationErrorDetail error = new ValidationErrorDetail(
    "query",                  // Fehler bezieht sich auf die Query-String
    "size",                   // Name des Query-Parameters aus der öffentlichen API
    "size darf nicht größer als 100 sein"
);

Und der dritte Typ – der Path-Parameter (z.B. taskId). Wenn taskId nicht wie eine UUID aussieht (oder Ihre Validierung nicht besteht), dann ist es logisch, den Pfad als taskId zu setzen. Wieder: Es ist der Name des Vertragsparameters, nicht der Variablenname im Java-Code.

ValidationErrorDetail error = new ValidationErrorDetail(
    "path",                   // Fehler bezieht sich auf die Path-Parameter der URL
    "taskId",                 // Name des Parameters im Vertrag: /tasks/{taskId}
    "taskId hat ein ungültiges Format"
);

Beachten Sie einen subtilen Punkt. Wir machen keinen Pfad der Form /api/v1/tasks/{taskId}. Das wäre „wo es passiert ist“, aber nicht „was zu korrigieren ist“. Der Pfad des Validierungsfehlers sollte auf die Eingabedaten verweisen, nicht auf den Endpunkt.

Wenn man dies mit „JSON-Augen“ sehen möchte, kann man folgendes Bild im Kopf behalten:

{
  "title": "",
  "description": "ok",
  "tags": ["java"]
}

Das Feld title ist genau das path="title". Keine Mystik: einfach die Adresse innerhalb der Eingabe.

3. Verschachtelte Strukturen: Notation parent.child

Sobald Sie eine verschachtelte Struktur haben, reicht ein einfacher Feldname nicht mehr aus. Und hier ist es wichtig, sich auf einen Stil zu einigen. Der für Menschen verständlichste und ziemlich universelle Stil ist die Punktnotation, bei der die Verschachtelung durch einen Punkt angegeben wird: assignee.email, metadata.description, contact.phone. Dies erinnert an den Zugriff auf Objektfelder im Code und ist leicht lesbar.

Auch wenn in der aktuellen Version des Task Tracker API die meisten DTOs flach sind, haben wir verschachtelte DTOs bereits auf Validierungsebene besprochen, und solche Strukturen können durchaus in anderen Anfragen (oder in zukünftigen Erweiterungen des Projekts) auftauchen. Wichtig ist nicht, ob es jetzt in jedem Endpoint eine Verschachtelung gibt, sondern dass Sie eine Grammatik wählen, die nicht beim ersten verschachtelten Objekt zusammenbricht.

Ein kleiner Lehrbeispiel (um die Idee im Code zu sehen). Stellen wir uns vor, dass die Anfrage zum Erstellen einer Aufgabe einen verschachtelten Block des Bearbeiters hat:

// Verschachteltes Request-Objekt: eigene Felder und eigene Validierung
record AssigneeRequest(String name, String email) {}

record TaskCreateRequest(
    String title,
    AssigneeRequest assignee // verschachteltes Objekt in JSON: { "assignee": { ... } }
) {}

Wenn assignee.email ein falsches Format hat, sollte path die vollständige Route anzeigen:

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",                        // JSON-Body
    "assignee.email",              // Pfad zum verschachtelten Feld über Punkt
    "E-Mail hat ein ungültiges Format"
);

Und so sieht es in JSON aus (für die Verbindung mit der Realität, nicht mit Abstraktion):

{
  "title": "Fix bug",
  "assignee": {
    "name": "Ivan",
    "email": "not-an-email"
  }
}

Die Punktnotation liest sich hier fast wie eine Anweisung: „gehe zu assignee, dann zu email“. Und genau das braucht der Kunde: Er weiß, welches Feld er in der Form hervorheben und wo er das Problem suchen muss.

Es gibt die Versuchung, den Pfad im JSON-Pointer-Stil (/assignee/email) oder „wie in Java“ (assigneeEmail) zu gestalten. Aber je mehr Varianten Sie zulassen, desto schneller wird der Kunde verwirrt. Deshalb wählen wir im Kurs ein Format und halten es stabil. Für Verschachtelung – Punkt.

4. Sammlungen und Indizes: tags[0] und tags[1]

Sobald eine Liste in den Eingabedaten erscheint, wird der Fehlerpfad ohne Index zu allgemein. Der Fehler „in tags“ ist wie die Nachricht „Sie haben ein Problem im Array“ in Java: Danke, aber wo genau? Die Validierung einer Sammlung ist oft zweierlei: Einschränkungen für die Liste selbst (z.B. „nicht mehr als 10 Tags“) und Einschränkungen für die Elemente (z.B. „jedes Tag Länge 1..30“). Und außerdem gibt es die Eindeutigkeit der Tags, wo ein „Elementkonflikt“ auftritt.

Für Elementfehler benötigen wir einen Index. In der aus der Java-Welt vertrauten Form – tags[0], tags[1] und so weiter. Wir betrachten die Indizierung als 0-basiert, weil dies ein natürliches Modell für Java-Sammlungen und die meisten technischen Formate ist. Und ja, dies ist der seltene Moment, in dem „wie in der Programmierung“ auch für den Kunden praktisch ist: Entwickler auf der Client-Seite zählen auch eher von null.

Zuerst das einfachste Beispiel: Das zweite Tag ist leer.

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",
    "tags[1]",              // Index des konkreten Listenelements (0-basiert)
    "Tag darf nicht leer sein"
);

Wenn Sie nur tags zurückgeben, müsste der Kunde das Array durchgehen und raten, wo leer ist. Wenn Sie tags[1] zurückgeben, versteht er sofort: „Das Element mit Index 1 ist defekt“.

Nun ein Beispiel aus einer echten Projektregel: Tags müssen ohne Berücksichtigung der Groß-/Kleinschreibung eindeutig sein. Angenommen, der Kunde hat gesendet:

{
  "title": "Write tests",
  "tags": ["java", "Java"]
}

Formal gesehen ist das Problem „zwischen den Elementen“, aber der Benutzer muss trotzdem die konkrete Stelle hervorgehoben bekommen. Im Lehr-API können wir den Fehler an das zweite sich wiederholende Element binden:

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",
    "tags[1]", // heben den „zweiten Versuch“ hervor, ein bereits vorhandenes Tag zu wiederholen
    "Tags müssen eindeutig sein (ohne Berücksichtigung der Groß-/Kleinschreibung)"
);

Manchmal macht man es anders: bindet den Fehler an beide Elemente (tags[0] und tags[1]), oder macht einen Objektpfad wie tags, oder sogar tagsUnique. Aber die Hauptsache ist, dass der Index Ihnen hilft, die Genauigkeit nicht zu verlieren. Wenn die Liste zehn Tags enthält und zwei sich wiederholen, ist es für den Kunden sehr schwierig zu zeigen, „welche Wiederholung“ genau. Der Index ist Ihr Freund.

Es ist auch nützlich, den Fehler der gesamten Sammlung vom Fehler des Elements zu unterscheiden. Wenn der Kunde 25 Tags gesendet hat, aber maximal 10 erlaubt sind, dann bezieht sich der Fehler auf den „Container“, und der Pfad kann einfach tags sein:

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",
    "tags", // Fehler betrifft die gesamte Liste, nicht ein konkretes Element
    "Die Anzahl der Tags darf 10 nicht überschreiten"
);

Wenn jedoch in der Liste alles in Ordnung ist, aber ein Element zu lang ist – sollte der Pfad einen Index enthalten:

ValidationErrorDetail error = new ValidationErrorDetail(
    "body",
    "tags[3]", // Fehler in einem konkreten Element
    "Die Länge des Tags darf 30 Zeichen nicht überschreiten"
);

Damit die Grammatik nicht im Projekt auseinanderfällt (und damit Sie den String nicht an drei verschiedenen Stellen manuell zusammensetzen und an einer Stelle die ] vergessen), kann man einen kleinen Utility-Helfer einführen. Er ist nicht zwingend, aber er zeigt die Idee gut:

final class Path {
    static String field(String name) {
        // Einfaches Feld: "title"
        return name;
    }

    static String nested(String parent, String child) {
        // Verschachteltes Feld: "assignee.email"
        return parent + "." + child;
    }

    static String index(String list, int i) {
        // Listenelement: "tags[1]"
        return list + "[" + i + "]";
    }
}

Und verwenden Sie es so:

String p1 = Path.field("title");              // "title"
String p2 = Path.index("tags", 1);            // "tags[1]"
String p3 = Path.nested("assignee", "email"); // "assignee.email"

Ja, das sieht „zu einfach“ aus. Aber genau solche einfachen Vereinbarungen retten das Projekt plötzlich vor Chaos, wenn die Anzahl der Endpoints wächst und die Fehler zahlreich werden.

5. Object path: Fehler einer Gruppe von Feldern

Nicht alle Validierungsregeln können an ein einzelnes Feld gebunden werden. Manchmal bezieht sich der Fehler auf zwei (oder mehr) Werte gleichzeitig, weil das Problem in ihrer Kombination liegt. Dies ist ein klassisches Cross-Field-Szenario: zum Beispiel muss in der Suche dueAfter früher sein als dueBefore. Formal kann jedes Feld einzeln gültig sein (beide Daten sind korrekt), aber zusammen ergeben sie eine sinnlose Anfrage.

In solchen Fällen verwenden wir object path – einen logischen Pfad für den „Fehler auf Objektebene“. Er muss nicht mit dem realen Feld des eingehenden DTO übereinstimmen. Es ist eher der „Name der Regel“ oder der „Name der Datengruppe“. In unserem Kurs ist ein gutes Beispiel dateRange. Für den Kunden ist nicht so wichtig, welches Feld „schuld“ ist, sondern dass der Datumsbereich falsch ist.

Beispiel eines Fehlerdetails:

ValidationErrorDetail error = new ValidationErrorDetail(
    "query",
    "dateRange", // logischer Pfad: Fehler nicht eines Feldes, sondern ihrer Kombination
    "dueAfter muss vor dueBefore sein"
);

Aus UX-Sicht ist das praktisch: Der Kunde kann den Fehler neben beiden Feldern oder über dem Datumsfilterblock anzeigen, ohne zu entscheiden, welches Feld „mehr schuld“ ist.

Ein weiteres Beispiel für object path ist, wenn Sie nicht ein bestimmtes Feld prüfen, sondern die Konsistenz der Anfragestruktur. Angenommen, Sie haben die Regel „entweder q oder tag muss übergeben werden“ (dies ist ein bedingtes Beispiel zur Demonstration). Dann könnte der logische Pfad searchCriteria oder query sein, aber es ist besser, etwas Aussagekräftigeres zu wählen – zum Beispiel searchMode. Hauptsache, dieser Pfad ist stabil und lesbar.

Warum binden wir solche Fehler nicht an dueAfter oder dueBefore? Man könnte, aber dann erhalten Sie eine „künstliche Schuld“ eines Feldes, obwohl das Problem in der Kombination liegt. Außerdem riskieren Sie, sofort zwei Fehler zu erhalten, und der Benutzer sieht zwei rote Meldungen, obwohl der Fehler sinngemäß einer ist. Object path erlaubt es, „ein Problem“ sauber als „ein Detail“ zu halten.

6. path sollte in der Sprache der API sprechen, nicht in der Sprache von Java

Eine der unangenehmsten Fallen bei Validierungsfehlern ist, Pfade zurückzugeben, die nur der Server versteht. Zum Beispiel heißt ein Java-Feld assigneeName, aber in JSON haben Sie es irgendwann (oder wurden gebeten) assignee_name zu nennen. Oder ein Query-Parameter im Vertrag heißt dueBefore, aber in der Controller-Methode haben Sie das Argument before genannt. Der Kunde hat Ihren Java-Code nicht gesehen und muss ihn auch nicht sehen. Er hat den Vertrag gesehen.

Daher ist die Regel einfach: path sollte öffentliche Namen verwenden, also die, die in der API vorkommen. Wir haben bereits im Modul über DTO/Jackson besprochen, dass der externe Feldname vom internen Java-Namen abweichen kann. Deshalb muss path „auf der Seite der Außenwelt“ sein.

Eine Mini-Demonstration im Code. Angenommen, Sie erstellen ein DTO (wir diskutieren jetzt nicht, ob snake_case notwendig ist – das ist nicht das Thema der Lektion; uns ist das Prinzip wichtig):

import com.fasterxml.jackson.annotation.JsonProperty;

record TaskCreateRequest(
    String title,

    // Externer (öffentlicher) Feldname in JSON — "assignee_name"
    @JsonProperty("assignee_name")
    String assigneeName // interner Name in Java kann abweichen
) {}

Wenn die Validierung sich über assigneeName beschwert, dann sollte der Fehler aus Sicht des Kunden unter dem Pfad assignee_name kommen, denn so heißt das Feld in JSON. Sonst wird der Kunde die Antwort ansehen und denken: „Was ist assigneeName? Das habe ich nicht gesendet“. Und dann beginnt das Lieblingsspiel aller Entwickler: „Der Server sagt etwas, der Kunde sieht etwas anderes“.

Das Gleiche gilt für Query und Path. Wenn der Parameter im Vertrag taskId heißt, dann sollte er auch in der Antwort bei einem Fehler taskId sein, selbst wenn Sie die Variable im Code id genannt haben. Unser Ziel ist es, die Antwort verständlich zu machen, ohne die Serverquellen öffnen zu müssen.

7. Grammatik von path im Task Tracker API

Um path nicht in einen Zoo von Formaten zu verwandeln, ist es nützlich, die Regeln einmal festzulegen. Und dann – sie einfach nicht zu verletzen. Das klingt langweilig, wie „Formular 3-B ausfüllen“, aber in der Praxis beseitigt es eine ganze Klasse von Fehlern: verschiedene Controller geben unterschiedliche Pfade zurück, Clients brechen, Tests leben getrennt von der Realität, und dann fügt jemand einen Workaround hinzu wie „wenn path mit / beginnt, dann…“.

Unten ist eine kompakte „Pfadverfassung“ für unser Lehrprojekt. Sie ist nicht die einzig mögliche in der Welt, aber sie ist einfach genug, um im Kopf zu behalten, und ausdrucksstark genug, um Body/Query/Path und Verschachtelung abzudecken.

Situation im Eingang Wie sieht path aus Beispiel
Einfaches Feld (body) Name des JSON-Feldes title
Einfaches Feld (query) Name des Query-Parameters size
Einfaches Feld (path) Name des Path-Parameters taskId
Verschachteltes Feld parent.child assignee.email
Listenelement list[index] (0-based) tags[1]
Fehler auf Gruppenebene logischer Name dateRange

Und noch eine Tabelle, um genau die „Grammatik“ zu festigen, nicht die Beispiele:

Was wird ausgedrückt Symbol Warum so
Verschachtelung . leicht lesbar und aus dem Code vertraut
Index eines Listenelements [n] aus Sammlungen und Property-Pfad vertraut
„Gruppe von Feldern“ / Objektebene eigenständiger Name erlaubt es, kein „schuldiges Feld“ künstlich zu bestimmen

Aus diesen Regeln ergibt sich eine praktische Schlussfolgerung: path ist keine URL, kein JSONPath mit Sonderzeichen, kein interner Validator-Name und kein Java-Feldname „wie er gerade ist“. Es ist eine kompakte und stabile Adresse in den Eingabedaten des Kunden.

Wenn man die Vereinbarung einhält, werden Fehler sehr vorhersagbar. Sowohl in .http-Szenarien als auch in Postman und in Tests muss der Kunde nicht raten, wie der Server das Problem „nennt“ – er sieht genau das, was er gesendet hat, plus eine verständliche Navigation bis zur Fehlerstelle.

8. Typische Fehler beim Entwerfen von path

Fehler Nr. 1: zu allgemeiner Pfad wie request oder field.
Ein solcher path sieht aus wie der Versuch, „irgendwas zu schreiben“, aber nicht wie Hilfe für den Kunden. In der Folge kann die UI keine konkrete Stelle hervorheben, und der Client-Entwickler beginnt, Debug-Ausgaben zu drucken und manuell zu suchen, was nicht stimmt. Besser ist es, title, size, tags[1] oder assignee.email zurückzugeben und dieses Drama zu beenden, bevor es zu einer Serie mit 8 Staffeln wird.

Fehler Nr. 2: Fehlen von Indizes in Listen.
Wenn Sie tags anstelle von tags[3] zurückgeben, verstecken Sie den Hauptwert der Detailliste: Genauigkeit. Besonders schmerzhaft ist dies bei Eindeutigkeit oder falschem Elementformat. Der Kunde ist gezwungen, die gesamte Liste zu durchsuchen, um zu finden, was der Server „gemeint“ hat. Wenn Sie den Index bereits kennen – sagen Sie ihn einfach.

Fehler Nr. 3: Vermischung von Grammatiken in einem Projekt (tags.1 und tags[1]).
Die ersten paar Male ist das „nicht schlimm“. Dann tauchen Tests, Dokumentation, Frontend-Validierung auf, und plötzlich stellt sich heraus, dass an einer Stelle der Index in eckigen Klammern steht, an anderer Stelle mit Punkt, an dritter mit Unterstrich. Der Kunde muss mehrere Pfad-Parser unterstützen, und das ist bereits das Schmerzniveau „wir haben versehentlich eine Minisprache geschrieben, aber vergessen, Bescheid zu sagen“.

Fehler Nr. 4: Verwendung interner Java-Namen anstelle öffentlicher API-Namen.
Wenn Sie assigneeName zurückgeben, aber in JSON heißt es assignee_name (oder umgekehrt), beginnt der Kunde zu denken, dass der Server sich „über etwas anderes“ beschwert. Path muss in der Logik des öffentlichen Vertrags leben, sonst lassen Sie unbewusst das interne Modell nach außen durchsickern. Es ist dasselbe Problem wie „Domain-Modell direkt serialisieren“, nur in der Welt der Fehler.

Fehler Nr. 5: Versuch, ein „schuldiges Feld“ zu bestimmen, wo der Fehler in einer Kombination von Feldern liegt.
Bei Cross-Field-Regeln (z.B. Datumsbereich) ist es fast immer umstritten, den Fehler an ein Feld zu binden. Die Client-UI zeigt entweder zwei Fehler an oder einen, aber der Benutzer versteht nicht, warum genau dueAfter schuld ist und nicht dueBefore. Ein logischer Object Path wie dateRange erlaubt es, die Wahrheit zu sagen: Das Problem liegt in der Kombination, nicht in einem einzelnen Feld.

1
Aufgabe
Spring REST & MVC, Level 18, Lektion 1
Gesperrt
Punktpfad für ein verschachteltes Feld
Punktpfad für ein verschachteltes Feld
1
Aufgabe
Spring REST & MVC, Level 18, Lektion 1
Gesperrt
Index der Sammlung und Object Path in einer Antwort
Index der Sammlung und Object Path in einer Antwort
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION