JSON Toolkit

Kernpunkte für den Entwurf von JSON-API-Datenverträgen

Fazit: Sechs Dinge lassen sich einmal ausgeliefert sehr schwer ändern: Response-Hüllen, Fehlerformate, Paginierungsform, Darstellung von IDs und Zeit, die Semantik von null versus fehlend und die Versionsentwicklung.

Fortgeschritten~13 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für Kernpunkte für den Entwurf von JSON-API-Datenverträgen

Kurz gesagt: Legen Sie im Vertrag die Response-Hülle, das Fehlerformat, die Darstellung von IDs und Zeit sowie die Semantik von null versus fehlend fest, bevor Sie über Paginierung und Benennungsdetails streiten; wickeln Sie die oberste Ebene immer in eine data-Hülle, damit Paginierung und Tracing-IDs später ergänzt werden können, ohne bestehende Aufrufer zu brechen.

In der API-Entwurfsarbeit gibt es keine Standardantworten, aber es gibt einige „einmal gewählt, schwer zurückzunehmende“ Entscheidungen, die man besser durchdenkt, bevor man die erste Codezeile schreibt.

1. Response einhüllen oder nicht

Drei Stile:

// Style A: bare response
{ "id": 1, "name": "张三" }

// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }

// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Stil Vorteile Nachteile
Nackte Antwort Am einfachsten, eine Ebene weniger fürs Frontend Kann keine Metadaten anhängen; ein Array auf oberster Ebene birgt JSON-Hijacking- und Evolutionsrisiken
Datenhülle Leicht meta / errors / links ergänzbar Eine Verschachtelungsebene mehr, das Frontend muss res.data.data schreiben
Daten + Metadaten Beste Observability Erfordert strikte Einigung, welche Felder in meta gehören

Empfehlung: Einheitlich eine data-Hülle für Listen und Details verwenden und die oberste Ebene niemals zu einem Array machen. So bricht das spätere Ergänzen von Paginierung oder Tracing-IDs nie den Vertrag.

2. Fehlerformat: RFC 9457 kopieren

Eine eigene Fehlerstruktur zu erfinden ist eine riesige Quelle von Zusammenarbeitskosten. RFC 9457 (der RFC 7807 ersetzt) definiert application/problem+json:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Feld Bedeutung Erforderlich
type Stabile Problemtyp-URI Empfohlen
title Kurze menschenlesbare Zusammenfassung Empfohlen
status HTTP-Statuscode Empfohlen
detail Genaue Erklärung, was diesmal passiert ist Optional
instance Die Ressourceninstanz mit dem Problem Optional
Erweiterungsfelder Beliebige Geschäftsfelder Optional

Erweiterungsfelder (etwa balance / required oben) erlaubt die Spezifikation, und das Frontend kann sie für präzise Meldungen nutzen, statt den detail-Text zu parsen.

3. Paginierung: Cursor schlägt Offset

// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Dimension Offset-Paginierung Cursor-Paginierung
Implementierungsschwierigkeit Niedrig (LIMIT/OFFSET) Mittel (erfordert einen stabilen Sortierschlüssel)
Performance auf tiefen Seiten Schlecht (je größer der Offset, desto langsamer) Gut
Wenn sich Daten ändern Duplikate oder fehlende Einträge Stabil
Zu einer Seite springen Möglich Nicht möglich
Braucht eine Gesamtzahl Erfordert einen zusätzlichen Count Nicht erforderlich

Empfehlung: Offset-Paginierung für Admin-Tabellen (sie brauchen Seitensprünge und Summen) und Cursor-Paginierung für nutzerseitige Infinite Feeds.

4. IDs und Zeit

4.1 IDs immer als Strings

Die Begründung wurde im Artikel zu den Typen behandelt: Auf der JavaScript-Seite verlieren Werte oberhalb von 2^53 stillschweigend Präzision. Selbst wenn die aktuelle ID eine kleine Auto-Increment-Ganzzahl ist, lassen Strings Raum für einen späteren Wechsel zu Snowflake-IDs.

{ "id": "1000000000000000001" }

4.2 RFC-3339-Strings für Zeit

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • Keine Millisekunden-Timestamp-Zahlen verwenden: Sie sind schwer lesbar, und die Einheit (Sekunden/Millisekunden) wird häufig verwechselt;
  • Die Zeitzone ausdrücklich angeben: entweder mit Z oder mit einem Offset; niemals „lokale Zeit ohne Zeitzone“ verwenden;
  • Felder einheitlich als xxxAt benennen, damit auf einen Blick klar ist, dass es ein Zeitpunkt ist.

5. null und fehlend

Sie müssen eine Variante wählen und sie in die Dokumentation schreiben:

Ansatz „Kein Wert“ ausdrücken „Nicht ändern“ ausdrücken
Ansatz A Der Feldwert ist null Das Feld in der Anfrage weglassen
Ansatz B Das Feld existiert nicht Das Feld in der Anfrage weglassen

Das Problem von Ansatz A ist, dass „weggelassen“ und „als null gesendet“ in einem PATCH Unterschiedliches bedeuten, was leicht falsch interpretiert wird; das Problem von Ansatz B ist, dass JSON.stringify im Frontend Felder mit dem Wert undefined verwirft, was zufällig zur Semantik passt.

Praktische Empfehlung:

  • Ansatz A für Response-Bodies verwenden (null bedeutet klar keinen Wert, und das Frontend muss nicht undefined unterscheiden);
  • Ansatz B für Request-Bodies (insbesondere PATCH) verwenden, kombiniert mit Merge Patch oder JSON Patch, um die Semantik ausdrücklich zu machen.

6. Feldbenennung und Struktur

Entscheidung Übliche Wahl Vorbehalt
Benennungsstil camelCase (JS-Ökosystem) / snake_case (Python, Datenbanken) Projektweit einheitlich, nicht mischen
Enum-Werte Kleinbuchstaben mit Unterstrichen, "pending_review" Keine Zahlen verwenden; sie sind schwer lesbar
Boolean-Felder isActive / enabled Nicht status: 0/1 verwenden
Array-Felder Plurale Substantive, items Für leere Arrays [] zurückgeben, nie null
Verschachtelungstiefe Höchstens 3 Ebenen Tiefer aufteilen oder einen expand-Parameter anbieten

Für leere Arrays [] statt null zurückzugeben ist sehr wichtig: data.items.map(...) im Frontend stürzt dann nicht wegen eines null ab.

7. Evolutionsstrategie

Änderung Bricht Kompatibilität Behandlung
Ein optionales Feld ergänzen Nein Direkt ausliefern
Ein Pflichtfeld ergänzen Ja Einen Standardwert ergänzen oder per Version ausliefern
Ein Feld entfernen Ja Zuerst als veraltet markieren, Aufrufer beobachten, dann entfernen
Den Typ eines Feldes ändern Ja Einen neuen Feldnamen ergänzen und während der Übergangszeit doppelt schreiben
Die Enum-Wertemenge ändern Kommt darauf an Der Client muss einen Fallback für unbekannte Werte haben
Die Feldbedeutung ändern Ja Wie ein neues Feld behandeln

Drei Disziplinregeln:

  1. Clients müssen toleranter gegenüber unbekannten Feldern sein (nicht mit additionalProperties: false in die Enge treiben);
  2. Clients müssen einen Fallback-Zweig für unbekannte Enum-Werte haben;
  3. Veraltete Felder mindestens eine Hauptversion lang behalten und mit Deprecation in Response-Headern oder Dokumentation markieren.

8. Ein vollständiges Beispiel

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Diese Antwort spiegelt alle obigen Konventionen wider: data-Hülle, String-IDs, Beträge als Strings, Zeitstempel mit Zeitzone, null für fehlend, leeres Array, Cursor-Paginierung und meta für Observability-Informationen.

9. Checkliste

  • [ ] Die oberste Ebene ist kein Array; einheitlich eine data-Hülle verwenden;
  • [ ] Fehlerantworten folgen RFC 9457 und enthalten ein stabiles type;
  • [ ] Die Paginierungsform entspricht dem Geschäftsszenario;
  • [ ] IDs sind Strings, und Zeit verwendet RFC 3339;
  • [ ] Die Semantik von null und fehlend ist in die Dokumentation geschrieben;
  • [ ] Der Benennungsstil ist projektweit einheitlich, und Enums verwenden Kleinschreibungs-Strings;
  • [ ] Der Client hat Fallbacks für unbekannte Felder und unbekannte Enum-Werte;
  • [ ] Breaking Changes laufen über Versionierung oder eine Doppelschreib-Übergangszeit.

FAQ

Wie sollte ich eine weiterentwickelbare JSON-API entwerfen?

Wickeln Sie Listen und Details einheitlich in eine data-Hülle und machen Sie die oberste Ebene niemals zu einem Array, damit Paginierung und Tracing-IDs ergänzt werden können, ohne den Vertrag zu brechen. Verwenden Sie String-IDs und RFC-3339-Zeitstempel und schreiben Sie die Semantik von null versus fehlend in die Dokumentation. Halten Sie neue Felder optional und semantisch unabhängig und geben Sie Clients Fallbacks für unbekannte Felder und Enum-Werte.

Warum kein eigenes Fehlerformat erfinden?

Eine eigene Fehlerstruktur zu erfinden ist eine riesige Quelle von Zusammenarbeitskosten. Kopieren Sie application/problem+json nach RFC 9457, identifizieren Sie Probleme mit einem stabilen type und erlauben Sie Erweiterungsfelder wie balance und required, sodass das Frontend präzise Meldungen bauen kann, ohne den detail-Text zu parsen.

Sollte jeder Endpunkt Cursor-Paginierung verwenden?

Nein. Admin-Tabellen brauchen Seitensprünge und Summen, Offset-Paginierung passt also besser; nutzerseitige Infinite Feeds sollten Cursor-Paginierung verwenden, die auf tiefen Seiten stabil bleibt und Duplikate oder Lücken bei Datenänderungen vermeidet. Entscheiden Sie danach, ob das Szenario Seitensprünge und Summen braucht.