JSON Toolkit

JSON Schema का दोबारा उपयोग: $ref, $defs और allOf/oneOf Composition

मुख्य बात: $ref दोबारा उपयोग हल करता है, allOf और oneOf composition, और दोनों को एक साथ रखना वहीं टूटता है। इसमें $id, $defs, conditionals, recursion, bundling और debugging शामिल हैं।

मध्यम~11 मिनट पढ़ें2026-10-31 को प्रकाशित2026-10-31 को अपडेट किया गया
$ref, $defs और allOf/oneOf के साथ JSON Schema दोबारा उपयोग और composition का आरेख

सारांश: पहले साझा संरचनाओं को $ref के साथ $defs में निकालें, फिर allOf/oneOf की ओर बढ़ें; जब branches एक साथ मेल खा सकें तो anyOf या const टैग इस्तेमाल करें, और oneOf उन मामलों के लिए रखें जिनमें सचमुच ठीक एक मेल चाहिए।

Copy-paste दस मिनट बचाता है और तीन महीने बाद आपको एक चुपचाप बना दिया गया fork महँगा पड़ता है, जब किसी को याद नहीं रहता कि कौन-सी प्रति सही है।

1. दोबारा उपयोग की तीन परतें — फ़ाइलें बाँटने से पहले एक चुनें

फ़ील्ड बढ़ने लगें तो पहली प्रवृत्ति होती है हर बाधा एक ही फ़ाइल में ठूँस देना। यह ship करने का सबसे तेज़ तरीका है, पर दो-तीन सौ पंक्तियों के बाद हर फ़ील्ड बदलाव तीन जगह दोहराना पड़ता है। एक छूट गई और आपको मिलता है "locally पास, प्रोडक्शन में 500"। दोबारा उपयोग का अर्थ है कोई तथ्य एक बार लिखना और हर जगह उसका संदर्भ देना।

JSON Schema तीन परतें देता है, हर एक का दायरा अलग:

परत वाहक दायरा विशिष्ट उपयोग
Sub-structure $defs + $ref एक schema फ़ाइल Address, money, pagination, error आकार
फ़ाइल Standalone schema + $id एक service की कई फ़ाइलें User, order, product entities
Contract library Schema repository + bundled artifact कई services, कई भाषाएँ OpenAPI components, प्रकाशित contracts

अधिकांश प्रोजेक्ट्स के लिए पहली परत पर्याप्त है। फ़ाइलों में बाँटना तभी लाभ देता है जब दो services सचमुच एक ही contract साझा करें, या schema एक प्रकाशित artifact हो जिसे कोई दूसरी भाषा उपभोग करे। बहुत जल्दी बाँटने से $id resolution और सापेक्ष paths debug करना कठिन हो जाता है, और वह लागत सामान्यतः duplication टालकर बचाए गए परिश्रम से अधिक होती है।

एक सरल अनुभव-नियम: अगर कोई संरचना दो जगह दिखती है, एक प्रति सह्य है; जब तीसरी जगह दिखे तो उसे $defs में promote करें; केवल जब कोई दूसरी भाषा या repository उसे पढ़े, तभी वह अपने $id वाली standalone फ़ाइल की हक़दार है।

2. $id पहचान स्थापित करता है, $ref छलाँग लगाता है

$id टिप्पणी नहीं है। यह schema का base URI घोषित करता है। $ref मान एक URI reference है: इसे वर्तमान base URI के विरुद्ध resolve करके absolute URI बनाया जाता है, और फिर # के बाद का fragment किसी node को locate करता है। अगर base URI गलत है, तो हर सापेक्ष संदर्भ कहीं और इशारा करता है, फिर भी error सामान्यतः केवल "can't resolve reference" कहता है, गलत host का नाम नहीं लेता।

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "address": {
      "$anchor": "address",
      "type": "object",
      "required": ["city"],
      "properties": {
        "city": { "type": "string", "minLength": 1 },
        "detail": { "type": "string" }
      }
    }
  },
  "type": "object",
  "required": ["shipTo"],
  "properties": {
    "shipTo": { "$ref": "#/$defs/address" },
    "billTo": { "$ref": "#address" }
  }
}

shipTo JSON Pointer इस्तेमाल करता है जबकि billTo anchor; दोनों एक ही node पर पहुँचते हैं। नीचे की तालिका हर रूप को उसके resolution से जोड़ती है:

रूप किस ओर इशारा करता है Draft टिप्पणियाँ
# वर्तमान schema का root सभी Recursion के लिए आम
#/$defs/address $defs के नीचे node 2019-09 और बाद सबसे स्पष्ट; पसंदीदा
#address नामित anchor 2020-12 में $anchor Draft-07 में $id: "#address" लिखा जाता था
common.json base URI के सापेक्ष फ़ाइल सभी Register या bundle करना ज़रूरी
https://example.com/x.json Absolute URI सभी प्रोडक्शन में runtime fetch से बचें

एक सूक्ष्म जाल: अगर $defs के अंदर कोई subschema अपना $id घोषित करता है, तो वह उसके नीचे की हर चीज़ के लिए नया base URI बन जाता है। किसी अन्य फ़ाइल से नकल किया schema चिपकाएँ और बचा हुआ $id उसके सापेक्ष संदर्भों को मूल host की ओर मोड़ देगा — और validator को यह तभी पता चलता है जब वह उन्हें वास्तव में resolve करने की कोशिश करता है।

3. $defs एक container है, validation स्थल नहीं

$defs एक सामान्य map है। अंदर की कोई चीज़ तब तक प्रभावी नहीं होती जब तक कोई $ref वहाँ इशारा न करे। $defs के शीर्ष पर पड़ी required सूची, जिसका कोई आने वाला संदर्भ नहीं, चाहे कितनी ही सख्त दिखे, कुछ भी validate नहीं करती। यह विनिर्देश का इच्छित व्यवहार है, validator की bug नहीं।

{
  "$defs": {
    "money": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]{2}$",
      "description": "A two-decimal money string that avoids floating-point drift"
    },
    "page": { "type": "integer", "minimum": 1 }
  },
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "page": { "$ref": "#/$defs/page" }
  }
}

कुछ परंपराएँ टीम में तय करना इसके लायक हैं:

  • $defs के अंदर keys कोई semantics नहीं रखतीं। address, Address और addr सभी वैध हैं, इसलिए एक शैली पर सहमत हों।
  • 2019-09 से पहले keyword definitions था। $ref दोनों को लक्षित कर सकता है, पर strict mode definitions को अज्ञात keyword बता सकता है।
  • $defs कोई भी schema रख सकता है, जिसमें allOf fragment या अपने $id वाला स्वतंत्र subschema भी शामिल है।
  • $defs को template inheritance न मानें। यह केवल definitions संग्रहीत करता है; अर्थ अभी भी संदर्भ देने वाले context से आता है।

4. allOf, anyOf और oneOf एक-दूसरे के विकल्प नहीं हैं

तीनों में कई branches होती हैं, पर उनके स्वीकृति नियम अलग हैं। जब डेटा दो branches से मेल खाता है, allOf और anyOf पास होते हैं जबकि oneOf विफल होता है — लोग "schema टूटा हुआ है" निष्कर्ष निकालने का एकमात्र सबसे आम कारण।

Keyword न्यूनतम मेल अधिकतम मेल दो branches से मेल खाता डेटा Error गुणवत्ता विशिष्ट उपयोग
allOf सभी असीम पास अच्छी, एक बार में एक बाधा बाधा fragments जोड़ना
anyOf 1 असीम पास खराब, branches ढेर हो जाती हैं "Email या phone, इनमें से कोई"
oneOf 1 1 विफल खराब Discriminated unions
not 0 0 विफल सबसे खराब कुछ मानों को बाहर रखना; कम इस्तेमाल
{
  "allOf": [
    { "$ref": "#/$defs/auditable" },
    { "$ref": "#/$defs/softDeletable" },
    { "required": ["email"] }
  ],
  "unevaluatedProperties": false
}

allOf object definitions merge नहीं करता; यह केवल हर branch के पूरा होने की अपेक्षा करता है। हर branch स्वतंत्र रूप से मूल्यांकित होती है, इसलिए एक branch के अंदर additionalProperties: false केवल उन्हीं keys को जानता है जो उस branch ने घोषित कीं और दूसरी branch द्वारा घोषित फ़ील्ड को अतिरिक्त बताएगा। 2020-12 में समाधान है unknown-property नीति को शीर्ष स्तर पर रखना और unevaluatedProperties: false इस्तेमाल करना, जो सभी branches के बाद एक बार मूल्यांकित होता है।

anyOf का उपयोग "कई वैध आकार" के लिए करें, "कई परस्पर-अनन्य आकार" के लिए नहीं। अगर दो branches एक साथ मेल खा सकती हैं, तो oneOf वह कुख्यात अनुपयोगी संदेश देता है must match exactly one schema in oneOf.

5. if/then/else और discriminator: शर्तों को तय-योग्य बनाएँ

Conditional बाधाएँ oneOf से बेहतर errors देती हैं क्योंकि निर्णय-पथ स्पष्ट है: if पास होता है तो then लागू होता है; वरना else लागू होता है।

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": {
    "properties": { "kind": { "const": "image" } },
    "required": ["kind"]
  },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

तीन बारीकियाँ तय करती हैं कि यह इच्छित रूप से व्यवहार करता है या नहीं:

  1. if के अंदर properties entry key गायब होने पर विफल नहीं होती। "kind मौजूद है और image के बराबर है" व्यक्त करने के लिए इसे required के साथ जोड़ें।
  2. if स्वयं कभी validation error नहीं पैदा करता। उसका विफल होना बस else की ओर भेजता है, इसलिए यह अपने बल पर खराब डेटा नहीं रोक सकता।
  3. अकेला then या else निष्क्रिय है। ये केवल if के साथ लागू होते हैं।

जैसे-जैसे union branches बढ़ती हैं, conditional chain एक असहनीय nest बन जाती है। अधिक मज़बूत पैटर्न है हर branch के अंदर एक tag फ़ील्ड लॉक करना ताकि branches संरचना से ही परस्पर-अनन्य बनें:

{
  "oneOf": [
    {
      "properties": { "kind": { "const": "card" } },
      "required": ["kind", "cardNumber"]
    },
    {
      "properties": { "kind": { "const": "account" } },
      "required": ["kind", "accountId"]
    }
  ]
}

discriminator एक OpenAPI 3.x extension है, JSON Schema keyword नहीं। Ajv इसे तब समर्थित करता है जब discriminator: true option चालू हो, पर उसकी mapping mapping में रहती है और toolchains के पार portable नहीं है। अगर schema को कई validators में काम करना है, तो const टैग पैटर्न पसंद करें, जो किसी extension पर निर्भर नहीं करता।

6. Recursive references: trees, linked lists और एक-दूसरे को उद्धृत करने वाली फ़ाइलें

Recursive संरचनाओं को विशेष सिंटैक्स नहीं चाहिए — स्वयं पर वापस जाता $ref पर्याप्त है। Validators compile time पर references को अनंत विस्तारित नहीं करते; वे डेटा की वास्तविक गहराई के अनुसार recurse करते हैं, इसलिए कुछ दर्जन पंक्तियाँ किसी भी ऊँचाई का tree बता देती हैं।

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/tree-node.json",
  "$defs": {
    "node": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        }
      }
    }
  },
  "$ref": "#/$defs/node"
}

तीन सीमाएँ पहले से तय करना इसके लायक है:

  • डेटा गहराई: schema कोई सीमा नहीं लगाता, पर validation और serialization दोनों गहराई के अनुपात में stack खाते हैं। सार्वजनिक APIs को business सीमा जोड़नी चाहिए, मान लें आठ स्तर, वरना दुर्भावनापूर्ण गहरा payload stack overflow कर सकता है।
  • Mutual references: A, B को संदर्भित करता है और B, A को — यह वैध है, पर दोनों schemas पहले register होने चाहिए; वरना resolution क्रम परिणाम तय करता है।
  • Nullable branches: अगर कोई recursive फ़ील्ड null हो सकता है, तो "type": ["object", "null"] लिखें या उसे oneOf में लपेटें। $ref nullability विरासत में नहीं देता।

7. Cross-file references और bundling: प्रोडक्शन में schemas कभी fetch न करें

Cross-file references अक्सर विकास में "बस काम कर जाते हैं" क्योंकि validator लक्ष्य को $id से network पर fetch करता है। Deploy समय पर network नीति बदलें और पूरी validation chain टूट जाती है। प्रोडक्शन का नियम सरल है: हर संदर्भित schema को in-process register या पहले से bundle करना होगा।

import Ajv2020 from 'ajv/dist/2020';
import addressSchema from './address.json' with { type: 'json' };
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({ allErrors: true, strict: true });

// पहले संदर्भित फ़ाइल register करें; key उसके $id के बराबर होनी चाहिए, वरना सापेक्ष refs resolve नहीं होंगे
ajv.addSchema(addressSchema, 'https://bbdrug.com/schemas/address.json');

// entry schema को module scope पर एक बार compile करें
export const validateOrder = ajv.compile(orderSchema);

अगर order.json में $id: "https://bbdrug.com/schemas/order.json" है, तो उसका "$ref": "address.json" https://bbdrug.com/schemas/address.json पर resolve होता है, जो registered key से मेल खाता है। एक अक्षर का अंतर can't resolve reference देता है।

तीन व्यावहारिक तरीके ऐसे तुलना करते हैं:

तरीका कैसे ताकत लागत
Runtime पर register प्रति फ़ाइल addSchema कोई build step नहीं, फ़ाइलें स्वतंत्र रहती हैं Registration क्रम और keys मैन्युअल
Build time पर bundle ref parser से एक फ़ाइल में merge शून्य runtime निर्भरता बड़ा artifact, शोर भरे diffs
Async loading loadSchema + compileAsync अक्सर बदलते schemas संभालता है Network और cache की चिंताएँ; महत्वपूर्ण paths पर बचें

Frontend प्रोजेक्ट्स के लिए build time पर bundling अधिक सुरक्षित है। Server पर registration पर्याप्त है, बशर्ते एक test साबित करे कि हर reference resolve होता है। किसी भी तरह, CI में compile चलाएँ ताकि reference errors merge से पहले, पहली प्रोडक्शन request पर नहीं, सामने आएँ।

8. Debugging क्रम: resolve करें, compile करें, फिर branches देखें

लगभग हर $ref समस्या तीन चरणों में locate हो सकती है, और क्रम महत्वपूर्ण है:

लक्षण संभावित कारण समाधान
can't resolve reference ... from id ... $id में typo, गलत सापेक्ष base, लक्ष्य register नहीं $id और resolved absolute URI प्रिंट करें, फिर registered keys से तुलना करें
schema with key or id already exists वही $id दो बार register, या compile बार-बार कॉल Registration module init में ले जाएँ और एक बार चलाएँ
strict mode: unknown keyword: "definitions" draft-07 सिंटैक्स मिलाना $defs में बदलें; strict mode बंद न करें
डेटा दो oneOf branches से मेल खाता है Overlapping branch बाधाएँ, कोई tag फ़ील्ड नहीं हर branch में const जोड़ें, या anyOf पर switch करें
$defs के अंदर बाधाओं का कोई प्रभाव नहीं कोई उन्हें संदर्भित नहीं करता संदर्भ पथ जाँचें; $defs स्वयं कुछ validate नहीं करता

Debug करते समय message से पहले schemaPath पढ़ें: यह विफल branch और keyword का नाम बताता है और अंग्रेज़ी गद्य से कहीं अधिक विश्वसनीय है। Compiled validator को module scope पर cache करें, हर request पर दोबारा compile न करें। हर schema बदलाव के लिए पास होने और विफल होने दोनों मामले कवर करें — केवल happy path जाँचना कुछ नहीं जाँचना है। Validator चयन और error-message design के लिए JSON Schema: मूल बातों से प्रोडक्शन तक देखें; schema को type generation में जोड़ने के लिए Schema और TypeScript Types के बीच दो-तरफ़ा Sync देखें।

9. अक्सर पूछे जाने वाले प्रश्न

क्या $ref type या description के साथ रह सकता है?

2020-12 $ref को sibling keywords के साथ अनुमति देता है, और वे siblings सामान्य रूप से मूल्यांकित होते हैं। draft-07 में siblings अनदेखा होते हैं और केवल $ref लागू होता है। एक ही schema जब drafts के पार उपयोग हो, तो यह सबसे बड़ा व्यवहार-अंतर है, इसलिए migrate करने से पहले पुष्टि करें कि आपका validator कौन-सा draft समर्थित करता है।

मैं $defs या definitions इस्तेमाल करूँ?

नए काम के लिए $defs इस्तेमाल करें; यह 2019-09 के साथ आया। definitions 2020-12 में keyword नहीं है। $ref: "#/definitions/foo" जैसा pointer फिर भी resolve हो सकता है क्योंकि यह सादा JSON Pointer है, पर strict mode अज्ञात keyword बताएगा। दोनों शैलियाँ मिलाना एक बारूद छोड़ देता है जो केवल CI में फटता है।

oneOf मान्य दिखने वाला डेटा क्यों अस्वीकार करता है?

क्योंकि डेटा एक साथ दो branches को संतुष्ट करता है। oneOf ठीक एक की माँग करता है, और object के केवल एक हिस्से को सीमित करने वाली branch तब भी संतुष्ट होती है जब दूसरी branch के फ़ील्ड अनुपस्थित हों। उन्हें परस्पर-अनन्य बनाने के लिए हर branch में const टैग जोड़ें।

क्या recursive references अनंत loop में चलेंगे?

नहीं। Validators compile time पर references विस्तारित नहीं करते; वे वास्तविक डेटा पर recurse करते हैं। असली जोखिम हैं stack overflow करने लायक गहरा डेटा और आपस में संदर्भित फ़ाइलों के बीच गलत resolution क्रम। Recursive फ़ील्ड पर business-स्तर की गहराई सीमा किसी भी schema तरकीब से अधिक प्रभावी है।

क्या प्रोडक्शन remote $ref पर निर्भर रह सकता है?

नहीं। Remote references validation की उपलब्धता को network से बाँध देते हैं: domain बदलाव, expired certificates और आंतरिक network isolation सभी chain तोड़ देते हैं। अनुमानित व्यवहार चाहिए तो संदर्भित schemas को artifact में bundle करें या startup पर register करें।

$defs और अलग फ़ाइलों के बीच कैसे तय करूँ?

दो जगह दिखने वाली संरचना एक बार नकल की जा सकती है; जब वह तीसरी जगह दिखे तो उसे $defs में promote करें। केवल जब कोई दूसरी भाषा या दूसरा repository उसे पढ़े, तभी वह अपने $id वाली standalone फ़ाइल की हक़दार है।

$defs के अंदर बाधाओं का कोई प्रभाव क्यों नहीं?

$defs केवल एक container है, और जिन nodes की ओर कोई $ref इशारा नहीं करता वे validation में कभी भाग नहीं लेते। $defs के शीर्ष पर पड़ी required सूची, जिसका कोई आने वाला संदर्भ नहीं, फिर भी डेटा पास होने देती है — यह विनिर्देश है, validator की bug नहीं।

क्या मुझे allOf branches के अंदर additionalProperties: false रखना चाहिए?

नहीं। हर branch स्वतंत्र रूप से मूल्यांकित होती है, इसलिए एक branch दूसरी branch द्वारा घोषित फ़ील्ड को अतिरिक्त मानती है। 2020-12 का तरीका unknown-field नीति को शीर्ष स्तर पर ले जाना और unevaluatedProperties: false इस्तेमाल करना है, जो सभी branches के बाद एक बार मूल्यांकित होता है।