JSON Toolkit

JSON कॉन्ट्रैक्ट का वर्ज़निंग: संगतता, डेप्रीकेशन, और डेटा माइग्रेशन

मुख्य बात: फ़ील्ड जोड़ना हमेशा स्वतः सुरक्षित नहीं होता। संगतता सीमाएँ, वर्ज़न फ़ील्ड, डेप्रीकेशन विंडो, dual-write माइग्रेशन, कॉन्ट्रैक्ट परीक्षण और canary मेट्रिक्स को कवर करता है।

उन्नत~10 मिनट पढ़ें2026-10-31 को प्रकाशित2026-10-31 को अपडेट किया गया
JSON कॉन्ट्रैक्ट, संगतता, और डेटा माइग्रेशन के वर्ज़निंग का कवर

मुख्य निष्कर्ष: वर्ज़न विकास को expand, backfill, फिर contract के रूप में चलाएँ, और वर्ज़न को दस्तावेज़ के अंदर ही स्थायी रखें; कुछ भी भेजने से पहले पुष्टि करें कि उपभोक्ता अज्ञात फ़ील्ड और enum मान सहते हैं, और डेप्रीकेशन को freeze, announce, observe, और remove के क्रम में ले जाएँ।

वर्ज़निंग का कठिन हिस्सा कोड बदलना नहीं है; यह है कि आप नहीं जान सकते कि पुराना फ़ील्ड अब भी कौन पढ़ता है। संगतता कभी एकतरफ़ा वादा नहीं है — यह प्रकाशक और अज्ञात उपभोक्ताओं के बीच चल रही तालमेल है।

1. संगतता सीमाएँ: योज्य स्वतः सुरक्षित नहीं

पाठ्यपुस्तकें एक नए वैकल्पिक फ़ील्ड को "पश्च-संगत" कहती हैं, लेकिन यह तभी टिकता है जब उपभोक्ता अज्ञात फ़ील्ड सहते हों। फ़ील्ड जोड़ना कम से कम तीन स्थितियों में क्लाइंट तोड़ता है:

  • उपभोक्ता ऐसी स्कीमा से सत्यापन करता है जो additionalProperties: false सेट करती है;
  • उपभोक्ता ऑब्जेक्ट को निश्चित क्रम में चलता है, या कुंजी के बजाय स्थिति से मान पढ़ता है;
  • नया फ़ील्ड पुराना नाम पुनः उपयोग करता है लेकिन उसका अर्थ बदलता है, उदाहरण के लिए status को "ऑर्डर स्थिति" से "फ़ुलफ़िलमेंट स्थिति" तक चौड़ा करना।

एक सुरक्षित जोड़ को तीन पूर्व-शर्तें चाहिए: फ़ील्ड वैकल्पिक हो, उसका अर्थ स्वतंत्र हो, और उपभोक्ता अज्ञात कुंजियों को अनदेखा करें। वर्ज़न संख्या पर चर्चा करने से पहले तीनों की पुष्टि करें।

{
  "schemaVersion": 2,
  "orderId": "ord-8842",
  "status": "paid",
  "refundDeadline": "2026-11-30T00:00:00+08:00",
  "channel": "wechat"
}

यहाँ refundDeadline और channel वैकल्पिक v2 जोड़ हैं, जबकि orderId और status अपने मूल अर्थ रखते हैं। schemaVersion दस्तावेज़ के साथ स्थायी रूप से रखा जाता है, केवल HTTP हेडर में जीवित नहीं।

2. ब्रेकिंग बदलाव जो आप भेज नहीं सकते

निम्नलिखित बदलावों को लाल रेखा मानें जिनके लिए नया वर्ज़न या dual-write संक्रमण चाहिए। दस्तावेज़ अपडेट करना आपको नहीं बचाएगा:

परिवर्तन संगतता क्यों टूटती है स्वीकार्य विकल्प
फ़ील्ड हटाएँ उपभोक्ता तुरंत undefined पढ़ते हैं Deprecate करें, देखें, फिर विंडो के बाद हटाएँ
फ़ील्ड का प्रकार बदलें "1" बन जाता है 1; सख़्त टाइप क्लाइंट सीधे विफल, और ढीले टाइप वाले अक्सर सत्यापन में विफल नया फ़ील्ड नाम जोड़ें और संक्रमण के दौरान dual-write करें
अनिवार्य फ़ील्ड जोड़ें पुराने कॉलर इसे नहीं भेजते, इसलिए सत्यापन हमेशा विफल डिफ़ॉल्ट दें, या इसे केवल नए वर्ज़न में अनिवार्य बनाएँ
enum मानों का समूह बदलें उपभोक्ता switch कथनों में fallback शाखा नहीं पहले क्लाइंट को अज्ञात मान स्वीकार करना सिखाएँ, फिर enum चौड़ा करें
फ़ील्ड का अर्थ बदलें प्रकार वैध रहते हैं जबकि व्यवहार बदलता है — डिबग करना सबसे कठिन मामला इसे नया फ़ील्ड मानें और पुराने अर्थ को फ़्रीज़ करें
नेस्टिंग गहराई बदलें हर पहुँच पथ बदलना पड़ता है पुराना आकार रखें और समतल फ़ील्ड या expand पैरामीटर जोड़ें

अर्थ बदलना सबसे ख़तरनाक पंक्ति है: स्कीमा सत्यापन हरा रहता है, मॉनिटरिंग कोई त्रुटि नहीं बताती, और केवल व्यावसायिक मेट्रिक्स ड्रिफ़्ट करते हैं। अर्थ में किसी भी बदलाव को नए फ़ील्ड के रूप में भेजना चाहिए।

3. वर्ज़न कहाँ रहता है: दस्तावेज़, URL, मीडिया प्रकार

तीन सामान्य स्थान अलग-अलग उद्देश्यों की सेवा करते हैं:

स्थान उदाहरण किसके लिए अच्छा सीमा
दस्तावेज़ फ़ील्ड "schemaVersion": 2 डेटाबेस, संदेश कतारें, ऑफ़लाइन बैकअप ट्रांसपोर्ट परत इस पर रूटिंग नहीं कर सकती
URL पथ /v2/orders/8842 सार्वजनिक API संसाधन पतों को वर्ज़न से बाँधता है और माइग्रेशन महँगा बनाता है
मीडिया प्रकार application/vnd.acme.order.v2+json आंतरिक सेवाएँ जिन्हें कॉन्टेंट नेगोशिएशन चाहिए कॉलर को Accept सही सेट करना होगा

एक व्यावहारिक श्रम विभाजन: वायर पर मीडिया-प्रकार नेगोशिएशन का उपयोग करें, और दस्तावेज़ में हमेशा वर्ज़न फ़ील्ड स्थायी रखें। कतार में पुराने संदेश URL बदलने से अपग्रेड नहीं होंगे, और न बैकअप फ़ाइलें। केवल दस्तावेज़-अंतर्गत schemaVersion ही replay, backfill, और audits को आधार देता है।

GET /orders/ord-8842 HTTP/1.1
Host: api.bbdrug.com
Accept: application/vnd.acme.order.v2+json

यदि आप URL का वर्ज़न करते हैं, तो कम से कम पूरी डेप्रीकेशन विंडो तक पुराने पथ को पुराना आकार लौटाने दें, उसे चुपचाप नए कार्यान्वयन पर इंगित करने के बजाय। व्यापक API कॉन्ट्रैक्ट निर्णयों के लिए, देखें JSON API डेटा कॉन्ट्रैक्ट डिज़ाइन के मूल बिंदु।

4. डेप्रीकेशन विंडो खोलना

डेप्रीकेशन एक ही घोषणा नहीं है; यह एक विंडो है जिसकी शुरुआत, अवलोकन अवधि, और अंत होता है। एक व्यावहारिक लय:

  1. Freeze: पुराना फ़ील्ड लिखना बंद करें, लेकिन पढ़ना जारी रखें।
  2. Announce: हटाने की तारीख दस्तावेज़, changelog, और प्रतिक्रिया हेडर में एक साथ चिह्नित करें।
  3. Observe: हर कॉलर द्वारा पुराने फ़ील्ड की पढ़त गिनें जब तक वह लगातार दो अवधियों तक शून्य पर न रहे।
  4. Remove: केवल विंडो बंद होने, पढ़त शून्य होने, और रिग्रेशन परीक्षण पास होने के बाद, फ़ील्ड और उसके संगतता कोड को हटाएँ।

Sunset प्रतिक्रिया हेडर (RFC 8594) क्लाइंट को मशीन-पठनीय हटाने की तारीख देता है। Deprecation अब भी IETF ड्राफ़्ट है: उपयोगी, लेकिन मान न लें कि हर क्लाइंट इसे समझता है। इसे माइग्रेशन गाइड की ओर इशारा करने वाले Link हेडर के साथ जोड़ें:

Deprecation: true
Sunset: Wed, 31 Mar 2027 23:59:59 GMT
Link: <https://bbdrug.com/docs/deprecations/order-status>; rel="deprecation"; type="text/html"

विंडो की लंबाई टीम की परंपरा है। सामान्य न्यूनतम एक रिलीज़ चक्र या 90 दिन, इनमें से जो लंबा हो। सार्वजनिक APIs को अधिक मिलना चाहिए, आंतरिक सेवाओं को कम, लेकिन कभी भी उस समय से कम नहीं जितना हर कॉलर को अपग्रेड करने में लगता है।

5. डेप्रीकेट करने से पहले उपभोक्ता खोजें

डेप्रीकेट करने से पहले, आपको "अब भी इसे कौन पढ़ता है?" का उत्तर देने में सक्षम होना चाहिए। अन्यथा विंडो आत्म-धोखा है। साक्ष्य को विश्वसनीयता से क्रमबद्ध करें:

  • Gateway लॉग: पुराने-फ़ील्ड पहुँच को appId, User-Agent, और क्लाइंट वर्ज़न से एकत्रित करें।
  • वितरित ट्रेस: सीरियलाइज़ेशन परत में पुराने फ़ील्ड को टैग करें ताकि ट्रेस सीधे कॉलिंग सेवा का नाम दिखाए।
  • कतार उपभोक्ता रजिस्ट्री: उपभोक्ता समूहों की सूची कोड खोज से अधिक विश्वसनीय है।
  • रिपॉज़िटरी खोज: rg केवल आपके अपने कोड को कवर करता है; बाहरी कॉलर के लिए अब भी लॉग चाहिए।
rg --type ts --type js 'legacyStatus|statusCode' src/ --stats

एक खोज परिणाम साबित करता है कि "इस रिपॉज़िटरी में कोई इसे पढ़ता है", न कि "कोई और नहीं पढ़ता"। Gateway लॉग दिन-प्रतिदिन निर्यात करें और कुछ भी हटाने से पहले पुष्टि करें कि पुराने-फ़ील्ड की पढ़त शून्य पर स्थिर रही है।

6. डेटा माइग्रेशन: expand, backfill, contract

Martin Fowler का Parallel Change तीन चरण बताता है: expand (पुराने और नए फ़ील्ड साथ रहते हैं और दोनों संस्करण पढ़े जा सकते हैं), backfill (ऐतिहासिक रिकॉर्ड को नए आकार में ले जाएँ), और contract (पुराना फ़ील्ड और संगतता शाखाएँ हटाएँ)। क्रम उलट न करें, अन्यथा आपके पास रोलबैक रास्ता नहीं होगा।

// माइग्रेशन कंकाल: प्राथमिक कुंजी से cursor, idempotency के लिए सशर्त writes
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 };
}

जिन विवरणों को चूकना आसान है:

  • Idempotency: filter में पुराना वर्ज़न संख्या शामिल करें, ताकि फिर से चलाने पर पहले से माइग्रेट किया गया रिकॉर्ड कभी दोबारा न लिखा जाए।
  • Batching और checkpoints: _id cursor से आगे बढ़ें, checkpoint संग्रहित करें, और विफलता के बाद उसी से फिर शुरू करें।
  • Rehearsal: उत्पादन स्नैपशॉट पर स्क्रिप्ट चलाएँ और पहले व बाद में रिकॉर्ड गिनती तथा checksums की तुलना करें।
  • Rollback: नया फ़ील्ड सत्यापन पास करने तक पुराना फ़ील्ड रखें; read paths को flag से स्विच करें और केवल schemaVersion संपादित करके कभी रोलबैक न करें।
  • Validation: माइग्रेशन के बाद व्यावसायिक बाधाएँ चलाएँ (ग़ैर-नकारात्मक राशियाँ, वैध स्थिति संक्रमण), केवल JSON parse जाँच नहीं।

कॉन्फ़िगरेशन डेटा भी इसी प्रक्रिया का पालन करता है; उन फ़ाइलों को व्यवस्थित करने के लिए देखें कॉन्फ़िगरेशन, fixtures और स्थानीय डेटा के लिए JSON प्रबंधित करना।

7. कॉन्ट्रैक्ट परीक्षण: संगतता को CI में डालें

संगतता कोड समीक्षा की स्मृति पर निर्भर नहीं हो सकती; उसे परीक्षण चाहिए। दो परतें पर्याप्त हैं:

  1. आकार परीक्षण: प्रतिक्रियाओं को JSON Schema से सत्यापित करें, लेकिन उपभोक्ताओं द्वारा उपयोग की जाने वाली स्कीमाओं पर additionalProperties: false न सेट करें।
  2. उपभोक्ता-उपसमूह परीक्षण: उन फ़ील्ड्स को निकालें जिन पर पुराना उपभोक्ता वास्तव में निर्भर है, एक उप-स्कीमा में, और असर्ट करें कि नई प्रतिक्रियाएँ अब भी उसे संतुष्ट करती हैं।
// कॉन्ट्रैक्ट परीक्षण: नई प्रतिक्रियाएँ अब भी पुराने उपभोक्ता उपसमूह को संतुष्ट करें
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('a v2 response still satisfies the v1 consumer subset', () => {
  const response = buildOrderResponse({ schemaVersion: 2 });
  const { schemaVersion, ...legacyFields } = response;
  const isValid = validateLegacySubset(legacyFields);

  assert.equal(isValid, true, JSON.stringify(validateLegacySubset.errors));
});

उप-स्कीमा केवल वे फ़ील्ड और प्रकार घोषित करती है जो पुराना उपभोक्ता पढ़ता है और हर नए फ़ील्ड को गुज़ारती है। यह हटाए गए फ़ील्ड और प्रकार परिवर्तन पकड़ती है, जोड़ों पर झूठे अलार्म के बिना। केस डेटा fixtures निर्देशिका में, क्रमबद्ध और diff-योग्य रखें; उन फ़ाइलों में मर्ज कॉन्फ़्लिक्ट के लिए, देखें JSON फ़ाइल Git कॉन्फ़्लिक्ट: union टूटी JSON लिखता है, कुंजी से मर्ज करें।

8. Canary रोलआउट: आयाम से बढ़ाएँ, मेट्रिक्स पर रोकें

संगतता समस्याएँ यूनिट परीक्षणों में कम ही सामने आती हैं; वे canary में सामने आती हैं। एक्सपोज़र को संकीर्ण से चौड़ा तक बढ़ाएँ:

आयाम अभ्यास क्या सत्यापित करता है
Tenant allowlist आंतरिक या मित्र ग्राहकों के लिए पहले नया read path सक्षम करें वास्तविक-दुनिया के डेटा आकार अंतर
क्लाइंट वर्ज़न केवल उन क्लाइंट के लिए सक्षम करें जो पहले से नए आकार में अपग्रेड हुए हैं पुराने और नए क्लाइंट साथ-साथ चलना
ट्रैफ़िक प्रतिशत 5% → 25% → 100%, हर चरण में एक पूरा व्यावसायिक चक्र देखें लॉन्ग-टेल कॉल और कैश हिट व्यवहार

कम से कम तीन मेट्रिक्स देखें: API त्रुटि दर, कॉन्ट्रैक्ट सत्यापन विफलता दर, और पुराने व नए फ़ील्ड के बीच पढ़त अनुपात। स्वचालित रोक शर्तें परिभाषित करें, जैसे 5xx 1% से ऊपर या पुराने-फ़ील्ड पढ़त में अचानक उछाल, और flag को रोलबैक करें। canary के दौरान dual writes चालू रखें ताकि रोलबैक के लिए केवल read path स्विच करना पड़े, डेटा सुधारना नहीं।

9. भेजने से पहले संगतता चेकलिस्ट

  • [ ] नए फ़ील्ड वैकल्पिक, अर्थ में स्वतंत्र हैं, और पुराने नाम पुनः उपयोग नहीं करते।
  • [ ] उपभोक्ता अज्ञात फ़ील्ड और अज्ञात enum मानों को fallback के साथ संभालते हैं।
  • [ ] वर्ज़न फ़ील्ड दस्तावेज़ के साथ स्थायी है और वायर पर नेगोशिएबल है।
  • [ ] डेप्रीकेशन विंडो में घोषणा, Sunset हेडर, और अवलोकन है।
  • [ ] पुराने-फ़ील्ड पढ़त को लॉग या ट्रेस से विशिष्ट कॉलर तक खोजा जा सकता है।
  • [ ] माइग्रेशन स्क्रिप्ट idempotent, batched, resumable है, और उसका रोलबैक रास्ता है।
  • [ ] कॉन्ट्रैक्ट परीक्षण पुराने उपभोक्ता उपसमूह को कवर करते हैं और CI में चलते हैं।
  • [ ] canary के स्पष्ट एक्सपोज़र आयाम और रोक मेट्रिक्स हैं।

FAQ

क्या वैकल्पिक फ़ील्ड जोड़ना हमेशा पश्च-संगत है?

नहीं। यह केवल तब संगत है जब उपभोक्ता अज्ञात फ़ील्ड अनदेखा करें और सख़्त स्कीमा सत्यापन से बचें। additionalProperties: false उपयोग करने वाला या स्थिति से पढ़ने वाला उपभोक्ता अब भी विफल होगा। भेजने से पहले कॉन्ट्रैक्ट परीक्षण से उनकी सहिष्णुता सत्यापित करें।

schemaVersion URL में रहना चाहिए या प्रतिक्रिया बॉडी में?

वायर मीडिया प्रकार या URL पथों से वर्ज़न नेगोशिएट कर सकता है, लेकिन संग्रहित रिकॉर्ड को दस्तावेज़ के अंदर schemaVersion रखना ही चाहिए। कतारें, ऑफ़लाइन बैकअप, और audits कभी URL से नहीं गुज़रते, इसलिए केवल दस्तावेज़-अंतर्गत वर्ज़न replay और backfill को सहारा दे सकता है।

क्या deprecated फ़ील्ड तुरंत हटाया जा सकता है?

नहीं। उसे लिखना बंद करें, हटाने की तारीख की घोषणा करें, और हर कॉलर की पढ़त गिनें। केवल विंडो बंद होने और पढ़त शून्य रहने के बाद हटाएँ। हटाने से पहले, पूरा रिग्रेशन चलाएँ और पुष्टि करें कि कोई छिपी cron जॉब या रिपोर्ट अब भी इसे नहीं पढ़ती।

अगर माइग्रेशन स्क्रिप्ट बीच में विफल हो जाए?

इसे idempotent जॉब के रूप में डिज़ाइन करें: सशर्त अपडेट, प्राथमिक-कुंजी cursor, और checkpoints। विफलता के बाद checkpoint से फिर शुरू करें, और दोहराए गए रन दो बार न लिखें। रोलबैक के लिए, पुराना फ़ील्ड और स्नैपशॉट रखें; केवल schemaVersion वापस न करें।

डेटा माइग्रेशन का क्रम कैसे रखना चाहिए?

Parallel Change का पालन करें: पहले expand करें ताकि पुराने और नए फ़ील्ड साथ रहें और दोनों संस्करण पढ़े जा सकें, ऐतिहासिक रिकॉर्ड नए आकार में backfill करें, फिर पुराना फ़ील्ड और संगतता शाखाएँ हटाकर contract करें। क्रम उलट न करें, अन्यथा रोलबैक रास्ता खो जाता है, और माइग्रेशन स्क्रिप्ट को idempotent, batched, और resumable बनाएँ।