Klíčové body návrhu datových kontraktů JSON API
Shrnutí: Šest věcí se po nasazení mění velmi těžko: obálky odpovědí, formáty chyb, tvar stránkování, reprezentace ID a času, sémantika null versus chybějící pole a vývoj verzí.

Stručně: zamkněte obálku odpovědi, formát chyb, reprezentaci ID a času a sémantiku null versus chybějící pole ve smlouvě dřív, než začnete diskutovat o stránkování a detailech pojmenování; nejvyšší úroveň vždy obalte obálkou data, aby stránkování a trasovací ID šla později přidat bez rozbití stávajících volajících.
V návrhu API neexistují standardní odpovědi, ale existují některá rozhodnutí „těžko vzít zpět po volbě“, která je nejlepší promyslet před napsáním prvního řádku kódu.
1. Obalovat odpověď, nebo ne
Tři styly:
// Styl A: samotná odpověď
{ "id": 1, "name": "张三" }
// Styl B: obálka data
{ "data": { "id": 1, "name": "张三" } }
// Styl C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
| Styl | Plusy | Mínusy |
|---|---|---|
| Samotná odpověď | Nejjednodušší, o vrstvu méně pro frontend | Nelze připojit metadata; pole na nejvyšší úrovni nese rizika JSON hijacking a evoluce |
| Obálka data | Snadné přidání meta / errors / links |
O úroveň vnoření více, frontend musí psát res.data.data |
| Data + metadata | Nejlepší pozorovatelnost | Vyžaduje striktní dohodu, která pole patří do meta |
Doporučení: jednotně používejte obálku data pro seznamy i detaily a nikdy nedělejte nejvyšší úroveň polem. Tím se pozdější přidání stránkování nebo trasovacích ID nikdy nedotkne smlouvy.
2. Formát chyb: zkopírujte RFC 9457
Vymýšlet vlastní strukturu chyb je obrovský zdroj nákladů na spolupráci. RFC 9457 (které nahrazuje RFC 7807) definuje 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 | Význam | Povinné |
|---|---|---|
type |
Stabilní URI typu problému | Doporučeno |
title |
Krátké shrnutí čitelné pro lidi | Doporučeno |
status |
HTTP stavový kód | Doporučeno |
detail |
Konkrétní vysvětlení, co se tentokrát stalo | Nepovinné |
instance |
Instance zdroje, která měla problém | Nepovinné |
| Rozšiřující pole | Libovolná obchodní pole | Nepovinné |
Rozšiřující pole (například balance / required výše) specifikace povoluje a frontend je může použít pro přesné zprávy, místo aby parsoval text detail.
3. Stránkování: kurzor vítězí nad offsetem
// Stránkování offsetem
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// Stránkování kurzorem
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| Dimenze | Stránkování offsetem | Stránkování kurzorem |
|---|---|---|
| Obtížnost implementace | Nízká (LIMIT/OFFSET) |
Střední (vyžaduje stabilní klíč řazení) |
| Výkon na hlubokých stránkách | Špatný (čím větší offset, tím pomalejší) | Dobrý |
| Když se data mění | Duplikáty nebo chybějící položky | Stabilní |
| Skok na stránku | Možný | Nemožný |
| Potřebuje celkový počet | Vyžaduje další počítání | Nevyžaduje |
Doporučení: stránkování offsetem použijte pro administrační tabulky (potřebují skoky na stránky a součty) a stránkování kurzorem pro uživatelské nekonečné feedy.
4. ID a čas
4.1 Pro ID vždy používejte řetězce
Odůvodnění pokrývá článek o typech: na straně JavaScriptu hodnoty nad 2^53 tiše ztrácejí přesnost.
I když je aktuální ID malé auto-increment celé číslo, použití řetězců nechává prostor pro pozdější přechod na snowflake ID.
{ "id": "1000000000000000001" }
4.2 Pro čas používejte řetězce RFC 3339
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- Nepoužívejte číselná časová razítka v milisekundách: jsou špatně čitelná a jednotka (sekundy/milisekundy) se často plete;
- Uveďte časové pásmo explicitně: buď
Z, nebo offset; nikdy nepoužívejte „místní čas bez časového pásma“; - Pole pojmenujte jednotně jako
xxxAt, aby bylo na první pohled zřejmé, že jde o bod v čase.
5. null a chybějící pole
Musíte zvolit jedno a zapsat to do dokumentace:
| Přístup | Vyjádření „žádná hodnota“ | Vyjádření „neměnit“ |
|---|---|---|
| Přístup A | Hodnota pole je null |
Vynechat pole v požadavku |
| Přístup B | Pole neexistuje | Vynechat pole v požadavku |
Problém přístupu A je, že „vynecháno“ a „posláno jako null“ znamenají v PATCH různé věci, což se snadno chybně interpretuje;
problém přístupu B je, že frontendový JSON.stringify zahazuje pole s hodnotou undefined, což náhodou odpovídá sémantice.
Praktické doporučení:
- Pro těla odpovědí použijte přístup A (
nulljasně znamená žádnou hodnotu a frontend nemusí rozlišovatundefined); - Pro těla požadavků (zejména
PATCH) použijte přístup B v kombinaci s Merge Patch nebo JSON Patch, aby byla sémantika explicitní.
6. Pojmenování polí a struktura
| Rozhodnutí | Běžné volby | Pozor |
|---|---|---|
| Styl pojmenování | camelCase (ekosystém JS) / snake_case (Python, databáze) |
Jednotně v projektu, nemíchat |
| Hodnoty výčtů | Malá písmena s podtržítky, "pending_review" |
Nepoužívejte čísla; špatně se čtou |
| Booleovská pole | isActive / enabled |
Nepoužívejte status: 0/1 |
| Pole polí | Množná podstatná jména, items |
Pro prázdná pole vracejte [], nikdy null |
| Hloubka vnoření | Maximálně 3 úrovně | Hlubší rozdělte nebo nabídněte parametr expand |
Vracení [] místo null u prázdných polí hodně záleží: data.items.map(...) na frontendu nespadne kvůli null.
7. Strategie vývoje
| Změna | Rozbíjí kompatibilitu | Jak řešit |
|---|---|---|
| Přidání volitelného pole | Ne | Nasaďte přímo |
| Přidání povinného pole | Ano | Přidejte výchozí hodnotu, nebo uvolněte po verzích |
| Odebrání pole | Ano | Nejprve označte deprecated, sledujte volající, poté odeberte |
| Změna typu pole | Ano | Přidejte nový název pole a během přechodu zapisujte do obou |
| Změna množiny výčtových hodnot | Záleží | Klient musí mít fallback pro neznámé hodnoty |
| Změna sémantiky pole | Ano | Považujte to za nové pole |
Tři pravidla disciplíny:
- Klienti musí být tolerantní k neznámým polím (neuzavírejte se do pasti
additionalProperties: false); - Klienti musí mít fallback větev pro neznámé výčtové hodnoty;
- Zastaralá pole držte alespoň jednu hlavní verzi a označte je
Deprecationv hlavičkách odpovědi nebo v dokumentaci.
8. Úplný příklad
{
"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 }
}
Tato odpověď odráží všechny konvence výše: obálku data, řetězcová ID, částky jako řetězce, časová razítka s časovým pásmem,
null znamenající chybějící, prázdné pole, stránkování kurzorem a meta pro informace o pozorovatelnosti.
9. Kontrolní seznam
- [ ] Nejvyšší úroveň není pole; jednotně používejte obálku
data; - [ ] Chybové odpovědi následují RFC 9457 a obsahují stabilní
type; - [ ] Tvar stránkování odpovídá obchodnímu scénáři;
- [ ] ID jsou řetězce a čas používá RFC 3339;
- [ ] Sémantika
nulla chybějícího pole je zapsána v dokumentaci; - [ ] Styl pojmenování je v projektu jednotný a výčty používají řetězce s malými písmeny;
- [ ] Klient má fallbacky pro neznámá pole a neznámé výčtové hodnoty;
- [ ] Rozbíjející změny procházejí verzováním nebo přechodem s dvojím zápisem.
Časté dotazy
Jak navrhnout vyvíjející se JSON API?
Seznamy i detaily jednotně obalte obálkou data a nikdy nedělejte nejvyšší úroveň polem, aby stránkování a trasovací ID šla přidat bez rozbití smlouvy. Používejte řetězcová ID a časová razítka RFC 3339 a sémantiku null versus chybějící pole zapište do dokumentace. Nová pole držte volitelná a sémanticky nezávislá a klientům dejte fallbacky pro neznámá pole a výčtové hodnoty.
Proč nevymýšlet vlastní formát chyb?
Vymýšlet vlastní strukturu chyb je obrovský zdroj nákladů na spolupráci. Zkopírujte application/problem+json z RFC 9457, identifikujte problémy stabilním type a povolte rozšiřující pole jako balance a required, aby frontend mohl stavět přesné zprávy bez parsování textu detail.
Má každý endpoint používat stránkování kurzorem?
Ne. Administrační tabulky potřebují skoky na stránky a součty, takže se lépe hodí stránkování offsetem; uživatelské nekonečné feedy mají používat stránkování kurzorem, které zůstává stabilní na hlubokých stránkách a vyhýbá se duplikátům nebo mezerám při změnách dat. Vybírejte podle toho, zda scénář potřebuje skoky na stránky a součty.