Kluczowe punkty projektowania kontraktów danych API JSON
Podsumowanie: Sześć rzeczy bardzo trudno zmienić po wdrożeniu: koperta odpowiedzi, format błędów, kształt paginacji, reprezentacja identyfikatorów i czasu, semantyka null a brak pola oraz ewolucja wersji.

Najważniejsze: ustal kopertę odpowiedzi, format błędów, reprezentację identyfikatorów i czasu oraz semantykę null a brak pola w kontrakcie, zanim zaczniesz debatować nad paginacją i szczegółami nazewnictwa; zawsze owijaj wierzchołek w kopertę data, aby paginację i identyfikatory śledzenia można było dodać później bez łamania istniejących wywołań.
W projektowaniu API nie ma standardowych odpowiedzi, ale istnieją decyzje „trudne do cofnięcia po wyborze”, które najlepiej przemyśleć przed napisaniem pierwszej linii kodu.
1. Kopertować odpowiedź czy nie
Trzy style:
// 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" } }
| Styl | Zalety | Wady |
|---|---|---|
| Odpowiedź naga | Najprostsza, o warstwę mniej dla frontendu | Nie można dołączyć metadanych; tablica wierzchołkowa niesie ryzyko JSON hijacking i ewolucji |
| Koperta data | Łatwo dodać meta / errors / links |
Jedna warstwa zagnieżdżenia więcej, frontend musi pisać res.data.data |
| Data + metadane | Najlepsza obserwowalność | Wymaga ścisłego ustalenia, które pola należą do meta |
Zalecenie: używaj jednolicie koperty data zarówno dla list, jak i szczegółów, i nigdy nie czyń wierzchołka tablicą. Dzięki temu dodanie paginacji lub identyfikatorów śledzenia później nigdy nie łamie kontraktu.
2. Format błędów: skopiuj RFC 9457
Wymyślanie własnej struktury błędów to ogromne źródło kosztu współpracy. RFC 9457 (które zastępuje RFC 7807) definiuje 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"
}
| Pole | Znaczenie | Wymagane |
|---|---|---|
type |
Stabilny URI typu problemu | Zalecane |
title |
Krótkie streszczenie czytelne dla człowieka | Zalecane |
status |
Kod statusu HTTP | Zalecane |
detail |
Konkretne wyjaśnienie, co się stało tym razem | Opcjonalne |
instance |
Instancja zasobu, która miała problem | Opcjonalne |
| Pola rozszerzeń | Dowolne pola biznesowe | Opcjonalne |
Pola rozszerzeń (takie jak balance / required powyżej) są dozwolone przez specyfikację, a frontend może ich użyć do precyzyjnych komunikatów, zamiast parsować tekst detail.
3. Paginacja: kursor bije offset
// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| Wymiar | Paginacja offset | Paginacja kursorowa |
|---|---|---|
| Trudność implementacji | Niska (LIMIT/OFFSET) |
Średnia (wymaga stabilnego klucza sortowania) |
| Wydajność na głębokich stronach | Słaba (im większy offset, tym wolniej) | Dobra |
| Gdy dane się zmieniają | Duplikaty lub brakujące elementy | Stabilna |
| Skok do strony | Możliwy | Niemożliwy |
| Wymaga całkowitej liczby | Wymaga dodatkowego zliczania | Niewymagane |
Zalecenie: używaj paginacji offsetowej dla tabel administracyjnych (potrzebują skoków stron i sum), a kursorowej dla nieskończonych kanałów dla użytkowników.
4. Identyfikatory i czas
4.1 Zawsze używaj łańcuchów dla identyfikatorów
Uzasadnienie omówiono w artykule o typach: po stronie JavaScript wartości powyżej 2^53 po cichu tracą precyzję.
Nawet jeśli obecny identyfikator to mała liczba całkowita autoinkrementacyjna, łańcuchy zostawiają miejsce na późniejsze przejście na identyfikatory snowflake.
{ "id": "1000000000000000001" }
4.2 Używaj łańcuchów RFC 3339 dla czasu
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- Nie używaj numerycznych znaczników czasu w milisekundach: są trudne do odczytania, a jednostkę (sekundy/milisekundy) często się myli;
- Określaj strefę czasową jawnie: albo przez
Z, albo przez przesunięcie; nigdy nie używaj „czasu lokalnego bez strefy czasowej”; - Nazywaj pola jednolicie
xxxAt, aby na pierwszy rzut oka było oczywiste, że to punkt w czasie.
5. null a brak pola
Musisz wybrać jedną zasadę i wpisać ją do dokumentacji:
| Podejście | Wyrażanie „braku wartości” | Wyrażanie „nie modyfikuj” |
|---|---|---|
| Podejście A | Wartość pola to null |
Pomiń pole w żądaniu |
| Podejście B | Pole nie istnieje | Pomiń pole w żądaniu |
Problem z podejściem A polega na tym, że „pominięte” i „wysłane jako null” znaczą co innego w PATCH, co łatwo źle zinterpretować;
problem z podejściem B polega na tym, że JSON.stringify w frontendzie pomija pola o wartości undefined, co przypadkiem pasuje do semantyki.
Praktyczne zalecenie:
- Używaj podejścia A dla treści odpowiedzi (
nulljasno znaczy brak wartości, a frontend nie musi rozróżniaćundefined); - Używaj podejścia B dla treści żądań (zwłaszcza
PATCH), w połączeniu z Merge Patch lub JSON Patch, aby semantyka była jawna.
6. Nazewnictwo i struktura pól
| Decyzja | Typowe wybory | Zastrzeżenie |
|---|---|---|
| Styl nazewnictwa | camelCase (ekosystem JS) / snake_case (Python, bazy danych) |
Jednolicie w projekcie, nie mieszaj |
| Wartości wyliczeń | Małymi literami z podkreśleniami, "pending_review" |
Nie używaj liczb; są trudne do odczytania |
| Pola logiczne | isActive / enabled |
Nie używaj status: 0/1 |
| Pola tablicowe | Rzeczowniki w liczbie mnogiej, items |
Zwracaj [] dla pustych tablic, nigdy null |
| Głębokość zagnieżdżenia | Nie więcej niż 3 poziomy | Głębiej niż tak, podziel to albo zaproponuj parametr expand |
Zwracanie [] zamiast null dla pustych tablic ma duże znaczenie: data.items.map(...) we frontendzie nie wysypie się z powodu null.
7. Strategia ewolucji
| Zmiana | Łamie zgodność | Jak obsłużyć |
|---|---|---|
| Dodanie pola opcjonalnego | Nie | Wdróż bezpośrednio |
| Dodanie pola wymaganego | Tak | Dodaj wartość domyślną albo wydaj przez wersję |
| Usunięcie pola | Tak | Najpierw oznacz jako przestarzałe, obserwuj wywołania, potem usuń |
| Zmiana typu pola | Tak | Dodaj nową nazwę pola i podwójny zapis w okresie przejściowym |
| Zmiana zbioru wartości wyliczenia | Zależy | Klient musi mieć fallback dla nieznanych wartości |
| Zmiana semantyki pola | Tak | Traktuj jak nowe pole |
Trzy zasady dyscypliny:
- Klienci muszą być tolerancyjni wobec nieznanych pól (nie zamykaj się w pułapkę przez
additionalProperties: false); - Klienci muszą mieć gałąź fallback dla nieznanych wartości wyliczeń;
- Przechowuj przestarzałe pola przez co najmniej jedną główną wersję i oznacz je nagłówkiem
Deprecationw odpowiedziach lub dokumentacji.
8. Kompletny przykład
{
"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 }
}
Ta odpowiedź odzwierciedla wszystkie powyższe konwencje: koperta data, identyfikatory łańcuchowe, kwoty jako łańcuchy, znaczniki czasu ze strefą czasową,
null znaczące brak, pusta tablica, paginacja kursorowa i meta dla informacji obserwowalności.
9. Lista kontrolna
- [ ] Wierzchołek nie jest tablicą; używaj jednolicie koperty
data; - [ ] Odpowiedzi błędów są zgodne z RFC 9457 i zawierają stabilne
type; - [ ] Kształt paginacji pasuje do scenariusza biznesowego;
- [ ] Identyfikatory są łańcuchami, a czas używa RFC 3339;
- [ ] Semantyka
nulli braku pola jest wpisana do dokumentacji; - [ ] Styl nazewnictwa jest jednolity w projekcie, a wyliczenia używają łańcuchów małymi literami;
- [ ] Klient ma fallbacki dla nieznanych pól i nieznanych wartości wyliczeń;
- [ ] Zmiany łamiące zgodność przechodzą przez wersjonowanie lub podwójny zapis.
FAQ
Jak zaprojektować ewoluowalne API JSON?
Owijaj jednolicie listy i szczegóły w kopertę data i nigdy nie czyń wierzchołka tablicą, aby paginację i identyfikatory śledzenia można było dodać bez łamania kontraktu. Używaj identyfikatorów łańcuchowych i znaczników czasu RFC 3339, a semantykę null a brak pola wpisz do dokumentacji. Zostaw nowe pola opcjonalne i semantycznie niezależne, a klientom daj fallbacki dla nieznanych pól i wartości wyliczeń.
Dlaczego nie wymyślać własnego formatu błędów?
Wymyślanie własnej struktury błędów to ogromne źródło kosztu współpracy. Skopiuj application/problem+json z RFC 9457, identyfikuj problemy stabilnym type i pozwalaj na pola rozszerzeń, takie jak balance i required, aby frontend mógł budować precyzyjne komunikaty bez parsowania tekstu detail.
Czy każde punkt końcowy powinien używać paginacji kursorowej?
Nie. Tabele administracyjne potrzebują skoków stron i sum, więc lepiej pasuje paginacja offsetowa; nieskończone kanały dla użytkowników powinny używać kursorowej, która jest stabilna na głębokich stronach i unika duplikatów lub luk przy zmianach danych. Wybieraj według tego, czy scenariusz wymaga skoków stron i sum.