JSON Toolkit

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.

Pokročilý~10 min čteníPublikováno 2026-10-31Aktualizováno 2026-10-31
Titulní obrázek článku Verzování kontraktů JSON, kompatibilita a migrace dat

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 status z „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:

  1. Freeze: přestněte staré pole zapisovat, ale dál ho čtěte.
  2. Oznamte: vyznačte datum odebrání v dokumentaci, changelogu a zároveň v hlavičce odpovědi.
  3. Pozorujte: počítejte čtení starého pole podle volajícího, dokud nezůstane na nule po dvě po sobě jdoucí období.
  4. 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-Agent a 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: rg pokrý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čí:

  1. Testy tvaru: validujte odpovědi pomocí JSON Schema, ale nenastavujte additionalProperties: false na schématech používaných konzumenty.
  2. 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 Sunset a 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ý.