Verzování kontraktů JSON: kompatibilita, deprecation a migrace dat
Shrnutí: Přidání pole není automaticky bezpečné. Pokrývá hranice kompatibility, verze v dokumentu, deprecation windows, migraci s dvojím zápisem, kontraktní testy a canary metriky.

Stručně: vývoj verzí veďte jako rozšíření, doplnění a uzavření (expand, backfill, contract) a verzi ukládejte do dokumentu; než cokoli nasadíte, potvrďte, že konzumenti tolerují neznámá pole a výčtové hodnoty, a deprecation proveďte v pořadí freeze, oznámení, pozorování a odebrání.
Obtížné na verzování není měnit kód; je to, že nemůžete vědět, kdo staré pole stále čte. Kompatibilita nikdy není jednostranný slib – je to průběžné slaďování vydavatele a neznámých konzumentů.
1. Hranice kompatibility: aditivní není automaticky bezpečné
Učebnice nazývají nové volitelné pole „zpětně kompatibilní“, ale to platí jen tehdy, když konzumenti tolerují neznámá pole. Přidání pole stále rozbíjí klienty minimálně ve třech situacích:
- konzument validuje schématem s
additionalProperties: false; - konzument prochází objekt v pevném pořadí nebo čte hodnoty podle pozice, nikoli podle klíče;
- nové pole znovu používá starý název, ale mění jeho význam, například rozšiřuje
statusz „stav objednávky“ na „stav vyřízení“.
Bezpečné přidání potřebuje tři předpoklady: pole je volitelné, jeho význam je nezávislý a konzumenti neznámé klíče ignorují. Potvrďte všechny tři, než vůbec začnete diskutovat o čísle verze.
{
"schemaVersion": 2,
"orderId": "ord-8842",
"status": "paid",
"refundDeadline": "2026-11-30T00:00:00+08:00",
"channel": "wechat"
}
Zde jsou refundDeadline a channel volitelná přidání v2, zatímco orderId a status si drží původní sémantiku. schemaVersion je uložen s dokumentem, místo aby žil jen v HTTP hlavičce.
2. Rozbíjející změny, které nemůžete nasadit
Následující změny považujte za červené čáry vyžadující novou verzi nebo přechod s dvojím zápisem. Aktualizace dokumentace vás nespasí:
| Změna | Proč rozbíjí kompatibilitu | Přijatelná alternativa |
|---|---|---|
| Odebrání pole | Konzumenti okamžitě čtou undefined |
Označte deprecated, pozorujte, poté po uplynutí okna odeberte |
| Změna typu pole | "1" se stane 1; silně typovaní klienti rovnou selžou a volně typovaní často selžou validací |
Přidejte nový název pole a během přechodu zapisujte do obou |
| Přidání povinného pole | Staří volající ho neposílají, takže validace vždy selže | Poskytněte výchozí hodnotu, nebo ho povinným udělejte jen v nové verzi |
| Změna množiny výčtových hodnot | switch konzumenta nemá fallback větev |
Nejdříve naučte klienty přijímat neznámé hodnoty, poté rozšiřte výčet |
| Změna významu pole | Typy zůstávají platné, zatímco chování se mění – nejhůře laditelný případ | Považujte to za nové pole a starou sémantiku zafixujte |
| Změna hloubky vnoření | Každá přístupová cesta se musí změnit | Ponechte starý tvar a přidejte plochá pole nebo parametr expand |
Změna významu je nejnebezpečnější řádek: validace schématem zůstává zelená, monitoring nehlásí chyby a driftují jen obchodní metriky. Každá změna významu má být nasazena jako nové pole.
3. Kde verze žije: dokument, URL, mediální typ
Tři běžná umístění slouží různým účelům:
| Umístění | Příklad | Hodí se pro | Omezení |
|---|---|---|---|
| Pole dokumentu | "schemaVersion": 2 |
Databáze, fronty zpráv, offline zálohy | Přenosová vrstva podle něj nemůže routovat |
| Cesta URL | /v2/orders/8842 |
Veřejná API | Svazuje adresy zdrojů s verzemi a činí migraci drahou |
| Mediální typ | application/vnd.acme.order.v2+json |
Interní služby vyžadující content negotiation | Volající musí správně nastavit Accept |
Praktické rozdělení práce: na drátě používejte vyjednávání mediálního typu a v dokumentu vždy ukládejte pole verze. Staré zprávy ve frontě se neupgradují proto, že jste změnili URL, a zálohy také ne. Jen schemaVersion v dokumentu dává přehrání, doplnění a auditům základ.
GET /orders/ord-8842 HTTP/1.1
Host: api.bbdrug.com
Accept: application/vnd.acme.order.v2+json
Pokud URL verzujete, alespoň po celé deprecation okno držte starou cestu vracející starý tvar, místo abyste ji tiše nasměrovali na novou implementaci. Širším rozhodnutím o smlouvě API se věnuje Klíčové body návrhu datových kontraktů JSON API.
4. Otevření deprecation okna
Deprecation není jedno oznámení; je to okno se začátkem, obdobím pozorování a koncem. Použitelný rytmus:
- Freeze: přestněte staré pole zapisovat, ale dál ho čtěte.
- Oznamte: vyznačte datum odebrání v dokumentaci, changelogu a zároveň v hlavičce odpovědi.
- Pozorujte: počítejte čtení starého pole podle volajícího, dokud nezůstane na nule po dvě po sobě jdoucí období.
- Odeberte: až po uzavření okna, když jsou čtení nulová a regresní testy projdou, smažte pole i jeho kompatibilní kód.
Hlavička odpovědi Sunset (RFC 8594) dává klientům strojově čitelné datum odebrání. Deprecation je stále IETF draft: použitelný, ale nepředpokládejte, že mu každý klient rozumí. Spárujte ji s hlavičkou Link ukazující na migrační příručku:
Deprecation: true
Sunset: Wed, 31 Mar 2027 23:59:59 GMT
Link: <https://bbdrug.com/docs/deprecations/order-status>; rel="deprecation"; type="text/html"
Délka okna je týmová konvence. Běžná spodní hranice je jeden release cyklus nebo 90 dní, podle toho, co je delší. Veřejná API by měla dostat víc, interní služby méně, ale nikdy méně než čas, který každý volající potřebuje k upgradu.
5. Najděte konzumenty, než začnete deprecovat
Než začnete deprecovat, musíte umět odpovědět „kdo to ještě čte?“ Jinak je okno sebeklam. Seřaďte důkazy podle spolehlivosti:
- Logy brány: agregujte přístupy ke starému poli podle
appId,User-Agenta verze klienta. - Distribuované traces: označte staré pole v serializační vrstvě, aby trace přímo ukázal název volající služby.
- Registr konzumentů fronty: seznam konzumentních skupin je spolehlivější než hledání v kódu.
- Hledání v repozitáři:
rgpokrývá jen váš vlastní kód; externí volající stále vyžadují logy.
rg --type ts --type js 'legacyStatus|statusCode' src/ --stats
Nález v hledání dokazuje „někdo v tomto repozitáři to čte“, nikoli „nikdo jiný ne“. Exportujte logy brány den po dni a potvrďte, že čtení starého pole zůstala na nule, než cokoli odeberete.
6. Migrace dat: expand, backfill, contract
Parallel Change od Martina Fowlera popisuje tři kroky: expand (staré a nové pole koexistují a obě verze lze číst), backfill (přesun historických záznamů do nového tvaru) a contract (odebrání starého pole a kompatibilních větví). Neobracejte pořadí, jinak nemáte cestu zpět.
// Skelet migrace: kurzor podle primárního klíče, podmíněné zápisy pro idempotenci
async function migrateBatch(collection, cursor, batchSize = 500) {
const records = await collection
.find({ _id: { $gt: cursor } })
.limit(batchSize)
.toArray();
if (records.length === 0) {
return { nextCursor: cursor, done: true };
}
const operations = records
.filter((record) => record.schemaVersion < 2)
.map((record) => ({
updateOne: {
filter: { _id: record._id, schemaVersion: record.schemaVersion },
update: {
$set: {
schemaVersion: 2,
status: record.legacyStatus ?? record.status,
refundDeadline: record.refundDeadline ?? null,
},
},
},
}));
if (operations.length > 0) {
await collection.bulkWrite(operations);
}
return { nextCursor: records.at(-1)._id, done: false };
}
Detaily, které snadno uniknou:
- Idempotence: zahrňte staré číslo verze do
filter, aby opakované spuštění nikdy nepřepsalo už migrovaný záznam. - Dávkování a checkpointy: posouvejte se kurzorem
_id, ukládejte checkpoint a po selhání z něj pokračujte. - Zkouška: spusťte skript proti produkčnímu snapshotu a srovnejte počty záznamů a kontrolní součty před a po.
- Rollback: držte staré pole, dokud nové pole neprojde validací; přepínejte čtecí cesty příznakem a nikdy nevracejte změnou samotného
schemaVersion. - Validace: po migraci spusťte obchodní omezení (nezáporné částky, legální přechody stavů), nikoli jen kontrolu parsování JSON.
Konfigurační data sledují stejný postup; jak tyto soubory organizovat, najdete v Správa JSON pro konfiguraci, fixtures a lokální data.
7. Kontraktní testy: dejte kompatibilitu do CI
Kompatibilita nesmí záviset na paměti při revizi kódu; potřebuje testy. Dvě vrstvy stačí:
- Testy tvaru: validujte odpovědi pomocí JSON Schema, ale nenastavujte
additionalProperties: falsena schématech používaných konzumenty. - Testy podmnožiny konzumenta: extrahujte pole, na která starý konzument skutečně spoléhá, do dílčího schématu a tvrďte, že nové odpovědi ho stále splňují.
// Kontraktní test: nové odpovědi musí stále splňovat podmnožinu starého konzumenta
import Ajv from 'ajv';
import assert from 'node:assert/strict';
import { test } from 'node:test';
const ajv = new Ajv({ strict: false });
const validateLegacySubset = ajv.compile(legacyConsumerSubsetSchema);
test('odpověď v2 stále splňuje podmnožinu konzumenta v1', () => {
const response = buildOrderResponse({ schemaVersion: 2 });
const { schemaVersion, ...legacyFields } = response;
const isValid = validateLegacySubset(legacyFields);
assert.equal(isValid, true, JSON.stringify(validateLegacySubset.errors));
});
Dílčí schéma deklaruje jen pole a typy, které starý konzument čte, a propouští každé nové pole. To zachytí odebraná pole i změny typů bez falešných poplachů u přidání. Držte případová data v adresáři fixtures, seřazená a diffovatelná; konfliktům slučování v těch souborech se věnuje Konflikty JSON v Git: union vytváří neplatný JSON, slučujte podle klíčů.
8. Canary nasazení: škálujte po dimenzích, zastavte podle metrik
Problémy s kompatibilitou se zřídka objeví v unit testech; objeví se v canary. Rozšiřujte expozici z úzké na širokou:
| Dimenze | Praxe | Co validuje |
|---|---|---|
| Allowlist tenantů | Zapněte novou čtecí cestu nejprve pro interní nebo spřátelené zákazníky | Rozdíly ve tvarech dat v reálném světě |
| Verze klienta | Zapněte jen pro klienty už upgradované na nový tvar | Staré a nové klienty běžící vedle sebe |
| Procento provozu | 5 % → 25 % → 100 %, při každém kroku pozorujte jeden plný obchodní cyklus | Long-tail volání a chování zásahů cache |
Sledujte alespoň tři metriky: chybovost API, míru selhání validace kontraktu a poměr čtení mezi starým a novým polem. Definujte automatické zastavovací podmínky, například 5xx nad 1 % nebo náhlý návrat čtení starého pole, a přepněte příznak zpět. Během canary držte dvojí zápis, aby rollback vyžadoval jen přepnutí čtecí cesty, nikoli opravu dat.
9. Kontrolní seznam kompatibility před nasazením
- [ ] Nová pole jsou volitelná, sémanticky nezávislá a znovu nepoužívají staré názvy.
- [ ] Konzumenti ošetřují neznámá pole a neznámé výčtové hodnoty fallbackem.
- [ ] Pole verze je uloženo s dokumentem a je na drátě vyjednatelné.
- [ ] Deprecation okno má oznámení, hlavičku
Sunseta pozorování. - [ ] Čtení starého pole lze dohledat ke konkrétním volajícím přes logy nebo traces.
- [ ] Migrační skript je idempotentní, dávkovaný, obnovitelný a má cestu rollback.
- [ ] Kontraktní testy pokrývají podmnožinu legacy konzumenta a běží v CI.
- [ ] Canary má explicitní expoziční dimenze a zastavovací metriky.
Časté dotazy
Je přidání volitelného pole vždy zpětně kompatibilní?
Ne. Je kompatibilní jen tehdy, když konzumenti neznámá pole ignorují a vyhýbají se striktní validaci schématem. Konzument používající additionalProperties: false nebo čtoucí podle pozice stále selže. Ověřte jejich toleranci kontraktním testem, než nasadíte.
Má schemaVersion žít v URL, nebo v těle odpovědi?
Na drátě lze vyjednávat verze pomocí mediálních typů nebo cest URL, ale uložené záznamy musí nést schemaVersion uvnitř dokumentu. Fronty, offline zálohy a audity nikdy neprocházejí URL, takže jen verze v dokumentu podporuje přehrání a doplnění.
Lze deprecated pole smazat okamžitě?
Ne. Přestněte ho zapisovat, oznamte datum odebrání a počítejte čtení podle volajícího. Smažte až po uzavření okna, když čtení zůstanou na nule. Před smazáním spusťte plnou regresi, abyste potvrdili, že ho nečte žádná skrytá cron úloha ani report.
Co když migrační skript selže v polovině?
Navrhněte ho jako idempotentní úlohu: podmíněné aktualizace, kurzor podle primárního klíče a checkpointy. Po selhání pokračujte z checkpointu a opakované běhy nesmějí zapisovat dvakrát. Pro rollback držte staré pole a snapshot; nejen vracejte schemaVersion.
Jak má být migrace dat seřazena?
Sledujte Parallel Change: nejprve expand, aby staré a nové pole koexistovalo a obě verze šly číst, poté backfill historických záznamů do nového tvaru, a nakonec contract odebráním starého pole a kompatibilních větví. Neobracejte pořadí, jinak ztratíte cestu rollback, a migrační skript udělejte idempotentní, dávkovaný a obnovitelný.