Auto-Generated JSON Schema असली डेटा क्यों अस्वीकार करता रहता है
मुख्य बात: Generated schemas अक्सर मान्य डेटा रोक देते हैं। जानें कि अधिक समृद्ध नमूनों से बदतर schemas क्यों बनते हैं, फिर workflow: multi-record sampling, semantic completion, error pinpointing.

सारांश: पहले required/optional की सीमा हाथ से तय करें — "all required" बंद करके 5–20 बहु-आकार वाले records दें — फिर schema बनाकर format, enum और additionalProperties मैन्युअल रूप से जोड़ें; एक ही पूर्ण-फ़ील्ड नमूना हमेशा बहुत सख्त रहेगा।
Generator केवल वही एक record देख सकता है जो आपने चिपकाया। वह "इस record में email है" को "हर record में email होना चाहिए" पढ़ता है, इसलिए आपके नमूने जितने पूर्ण होंगे, वह उतनी ही माँग करेगा।
1. Generator संरचनात्मक mapping करता है, business समझ नहीं
JSON का एक टुकड़ा generator को दें और वह ठीक एक काम करता है: मानों की संरचना में चलना और हर node को schema keywords में अनुवादित करना। पूरी mapping तालिका स्थिर है:
| नमूने में मान | अनुमानित type | टिप्पणियाँ |
|---|---|---|
"hello" |
{"type": "string"} |
email, date, UUID में अंतर नहीं करता |
42 |
{"type": "number"} |
integer को integer के रूप में नहीं समझता |
3.14 |
{"type": "number"} |
ऊपर जैसा ही |
true |
{"type": "boolean"} |
— |
null |
{"type": "null"} |
केवल null पहचानता है, "nullable" कभी नहीं |
[1, "a"] |
{"type": "array", "items": {"type": ["number", "string"]}} |
Element types union हो जाते हैं |
{"a": 1} |
{"type": "object", "properties": {...}, "required": [...]} |
required यहाँ की गाँठ है |
सारी मुसीबत अंतिम पंक्ति में है। required types से नहीं निकलता; यह "इस नमूने में यह key मौजूद है" से निकलता है। जब नमूना एक ही record हो, तो यह कथन "हर key अनिवार्य है" के बराबर है।
और असली APIs से लौटाया गया डेटा लगभग हमेशा आधे फ़ील्ड optional रखता है। यहीं टकराव शुरू होता है।
2. एक जैसे आकार के पाँच records, पाँच acceptance rates
माप से शुरू करें। नीचे के पाँच records देखकर कोई भी कहेगा "ये एक ही आकार के हैं":
[
{
"id": 1,
"name": "Ada",
"email": "ada@example.com",
"tags": ["a"],
"score": 10
},
{ "id": 2, "name": "Bob", "tags": [], "score": 7 },
{
"id": 3,
"name": "Cy",
"email": "cy@example.com",
"tags": ["a", "b"],
"score": 8.5
},
{ "id": 4, "name": "Dee", "email": null, "tags": ["c"], "score": 9 },
{
"id": 5,
"name": "Eve",
"email": "eve@example.com",
"tags": ["d"],
"score": 6,
"admin": true
}
]
email 5 में से 3 records में दिखता है, admin केवल एक बार दिखता है, और record 2 में tags का array खाली है।
अब हर record से अलग-अलग schema बनाएँ, फिर हर परिणाम से पाँचों records validate करें (validator: ajv 8):
| किससे बनाया | Key संख्या | बना required |
5 में से स्वीकार |
|---|---|---|---|
| Record 1 | 5 | id, name, email, tags, score |
3 records |
| Record 2 | 4 | id, name, tags, score |
5 records |
| Record 3 | 5 | id, name, email, tags, score |
3 records |
| Record 4 | 5 | id, name, email, tags, score |
1 record |
| Record 5 | 6 | id, name, email, tags, score, admin |
1 record |
पाँच में से कोई schema सही नहीं है, लेकिन सारा डेटा स्वीकार करने वाला केवल one record 2 से आता है — वही record जिसमें email गायब है। यह सीधे कारण से जीतता है: जो keys कभी दिखीं ही नहीं, वे required में कभी नहीं लिखी जातीं।
Record 4 और भी शिक्षाप्रद है: उसका email मान null है, इसलिए generator {"type": "null"} hardcode कर देता है, और स्ट्रिंग emails वाले बाकी चारों records अस्वीकार हो जाते हैं।
3. "सबसे पूर्ण record चिपकाना" सबसे बुरा विकल्प क्यों है
अंतर्ज्ञान कहता है "सबसे अधिक फ़ील्ड वाला नमूना चिपकाओ और schema पूर्ण हो जाएगा"। माप ठीक उल्टा दिखाते हैं — और एक तीसरे पक्ष ने बड़ा नियंत्रित परीक्षण चलाया।
absurdtools ने 3000 random "data families", प्रति family 8 records, हर optional फ़ील्ड लगभग आधे समय मौजूद, का उपयोग किया और मापा कि अलग key संख्या वाले नमूनों से बना schema कितना डेटा स्वीकार कर सकता है:
| नमूने में key संख्या | Family के डेटा का स्वीकार्य हिस्सा |
|---|---|
| 2 | 100.0% |
| 3 | 56.8% |
| 4 | 34.8% |
| 5 | 23.4% |
| 6 | 17.7% |
| 7 | 14.9% |
निष्कर्ष "थोड़ी गिरावट" नहीं, बल्कि पूरा उलटाव है: 95% families में न्यूनतम नमूने ने सबसे ढीला schema बनाया, जबकि सबसे पूर्ण नमूना यह केवल 0.1% बार कर सका।
कारण required की semantics में है, जिसका अर्थ "सभी" है; optionality केवल अनुपस्थिति से देखी जा सकती है। हर फ़ील्ड वाला record चिपकाना generator को बताता है "इनमें से कोई key छोड़ी नहीं जा सकती"; गायब फ़ील्ड वाला record चिपकाना उसे साक्ष्य देता है "यह key अनुपस्थित हो सकती है"।
एक वाक्य में याद रखें: नमूने का मूल्य पूर्णता नहीं, बल्कि यह है कि वह किन अनुपस्थितियों को कवर करता है।
कितने records पर्याप्त हैं
इसी प्रयोग-समूह ने convergence गति भी मापी: "कोई key तभी required रहती है जब तक अब तक के हर record में वह हो" इस नियम के तहत एक-एक record जोड़ते हुए, required के सच्चे required समूह तक पहुँचने के लिए records की median संख्या 3 है; 6 records लगभग नब्बे प्रतिशत families कवर करते हैं, और 12 सभी।
इसलिए रोज़मर्रा में 5–10 records sample करना प्रयास-बल का सर्वोत्तम मूल्य है; बीस या अधिक थोपना बहुत कम अतिरिक्त देता है।
पर एक पूर्व-शर्त है: ये records सचमुच अलग-अलग स्थितियों से आने चाहिए। एक ही template से नकल किए पाँच records दूसरे record से आगे कोई नई जानकारी देना बंद कर देते हैं — उनके फ़ील्ड समूह समान हैं, और required एक भी key से संकरी नहीं हो सकती।
4. समाधान: एक साथ कई records दें और अनुपस्थितियों को बोलने दें
चूँकि single-record sampling अनिवार्य रूप से बहुत सख्त है, सही तरीका है generator को एक साथ कई records देखने देना और required को "केवल तब required जब हर record में हो" नियम से converge कराना।
इस साइट का generator सीधे array स्वीकार करता है और उसमें "all required" toggle है। इसे बंद करें और यह occurrence count से तय करता है: केवल वे keys required में लिखी जाती हैं जो हर record में मौजूद हैं।
वही पाँच records — इस बार उन्हें array में मिलाकर एक साथ infer करें, फिर items को element contract मानें:
| कितने records दिए | अनुमानित required |
सभी 5 का validation |
|---|---|---|
| 1 record | id, name, email, tags, score |
3 पास |
| 2 records | id, name, tags, score |
4 पास |
| 3 records | id, name, tags, score |
4 पास |
| 4 records | id, name, tags, score |
5 पास |
| 5 records | id, name, tags, score |
5 पास |
चौथा record जोड़ते ही required converge हो जाती है, और उसके बाद नहीं बदलती। email और admin सही ढंग से properties में रखे जाते हैं पर required में नहीं। और चूँकि record 4 का email null है, multi-record inference उसे {"type": ["string", "null"]} में मिला देता है, इसलिए स्ट्रिंग और null दोनों पास होते हैं — जो single-record sampling कभी नहीं कर सकता।
यह absurdtools के इस निष्कर्ष के समान परिमाण का है कि "required median 3 records के बाद converge हो जाती है"।
Multi-record inference के अपने blind spots हैं: अगर इन records में email हर बार संयोग से null हो (मान लें सभी ऐसे accounts से हैं जिनकी profile भरी नहीं), तो inference परिणाम {"type": "null"} बन जाता है, और स्ट्रिंग emails फिर भी अस्वीकार होते हैं। इसी तरह, अगर हर tags मान खाली array है, तो element type का अनुमान ही नहीं लग सकता।
इसलिए sampling की एक और कड़ी शर्त है: हर फ़ील्ड का हर रूप कम से कम एक बार दिखना चाहिए। एक स्ट्रिंग, एक संख्या, एक null और एक खाली array लाएँ; किसी रूप को पूरी तरह अनुपस्थित न रहने दें।
व्यवहार में:
- logs, mocks या असली requests से 5–20 responses लें, और असामान्य पंक्तियाँ (खाली फ़ील्ड वाली, null वाली, खाली array वाली) ज़रूर शामिल करें;
- उन्हें JSON array में जमाएँ और generator में चिपकाएँ;
- "all required" बंद करें और draft 2020-12 चुनें;
- परिणाम
type: arrayऔर एकitemsहोता है;itemsवाला हिस्सा एकल record के contract के रूप में लें; - इसी schema से मूल नमूनों को तुरंत validate करें — 100% पास होना चाहिए। एक भी विफल हुआ तो आपका sampling अपर्याप्त है; वापस जाकर records जोड़ें।
चरण 5 सबसे सस्ता स्व-जाँच है। अगर generator वह डेटा भी नहीं संभाल सकता जो आपने दिया, तो आपकी sampling विधि गलत है, और आगे बढ़ने का कोई अर्थ नहीं।
5. चार चीज़ें जो generator नहीं भर सकता — आपको हाथ से लिखनी होंगी
कई records देने के बाद भी निम्नलिखित का अनुमान नहीं लगाया जा सकता और मैन्युअल रूप से जोड़ना पड़ता है:
| छूटा हुआ हिस्सा | मापा गया व्यवहार | परिणाम | समाधान |
|---|---|---|---|
| String format | "a@b.com" केवल {"type": "string"} देता है |
"not an email" का validation पास होता है |
"format": "email" जोड़ें और format assertions चालू करें |
| Ranges और enums | नहीं देख सकता कि score 0–100 तक सीमित है, न यह कि status के केवल तीन मान हैं |
ऋणात्मक संख्याएँ, विशाल मान और बेतुके enum मान सब पास होते हैं | minimum / maximum / enum जोड़ें |
| Unknown-field नीति | additionalProperties नहीं बनाया जाता |
गलत लिखी key चुपचाप स्वीकार हो जाती है, जो error से भी खतरनाक है | config-शैली फ़ाइलों के लिए "additionalProperties": false जोड़ें |
| Nullability और empty-collection semantics | null वाला एक नमूना {"type": "null"} पर लॉक हो जाता है; खाली array "items": {} बनाता है |
मान्य मान अस्वीकार होते हैं, या array elements पूरी तरह unconstrained रहते हैं | ["string", "null"] में बदलें, और खाली arrays को असली elements दें |
तीसरी पंक्ति अलग उल्लेख योग्य है: additionalProperties का डिफ़ॉल्ट मान "allow" है। इसका अर्थ है कि timeout को timeuot लिखने वाली config फ़ाइल validation पास कर लेती है और runtime पर चुपचाप कुछ नहीं करती। Config-शैली schemas के लिए यह ऐसा मद है जिसे स्पष्ट रूप से बंद करना ही होगा।
format में भी जाल है: draft 2019-09 से format annotation vocabulary का हिस्सा है, और डिफ़ॉल्ट रूप से यह केवल annotate करता है, लागू नहीं करता। अगर आपने format: "email" जोड़ा और फिर भी अवैध मान पास हो रहे हैं, तो दस में से आठ मामलों में validator ने validateFormats या संगत format plugin चालू नहीं किया — schema गलत नहीं है।
यहीं draft चयन पर एक टिप्पणी: अगर schema OpenAPI 3.1 में जा रहा है, तो उसे draft 2020-12 ही होना चाहिए, क्योंकि 3.1 सीधे वही version embed करता है; पुरानी toolchains (Swagger 2.0 ecosystem और कुछ $ref implementations) अभी भी draft-07 के इर्द-गिर्द हैं। $ref sibling keywords और array-संबंधित keywords पर दोनों का व्यवहार अलग है, इसलिए एक ही schema समूह में इन्हें न मिलाएँ।
6. अदस्तावेज़ित third-party APIs के लिए, response नमूनों से contract reverse-engineer करें
किसी third-party callback को integrate करना, दस्तावेज़ न लिखने वाले partner के साथ काम करना, या दस साल पुराना legacy सिस्टम बनाए रखना — इन सबका अर्थ है "डेटा हाथ में है, spec नहीं"। यहाँ generator केवल सुविधा नहीं; यह contract तक का सबसे तेज़ रास्ता है।
विधि ऊपर जैसी ही है, दो अतिरिक्त अनिवार्यताओं के साथ:
एक: समय के साथ और अवस्थाओं के पार sample करें। Third-party फ़ील्ड अक्सर business state के साथ बदलते हैं — promotion discount जोड़ता है, refund order refundAt जोड़ता है, और error response में data बिल्कुल नहीं होता। एक ही snapshot उठाने का अर्थ है केवल एक अवस्था देखना।
कुछ सफलताएँ, कुछ विफलताएँ और कुछ खाली परिणाम एक साथ दें: error और data अक्सर परस्पर-अनन्य रूप में दिखते हैं, और multi-record inference दोनों को properties में रखकर required से बाहर रखता है — वास्तविकता से ठीक मेल।
दो: schema को repository में रखें और रोज़ाना जाँच जोड़ें। आज के असली responses को उसके विरुद्ध validate करें, और आप अपने उपयोगकर्ताओं से पहले चुपचाप गायब होते या type बदलते फ़ील्ड के बारे में जान जाएँगे।
// दिन में एक बार चलाएँ: contract-drift alert
const drifted = records.filter((record) => !validate(record));
if (drifted.length > 0) {
console.warn(
`${drifted.length} responses do not match the contract; first difference:`,
validate.errors,
);
}
इस अभ्यास का मूल्य "हाथ से लिखने का परिश्रम बचाना" नहीं, बल्कि मौखिक समझौते को निष्पादन योग्य alert में बदलना है। Third party आपको पहले कभी नहीं बताता कि उसने कोई फ़ील्ड बदली, पर बदलाव निश्चित रूप से आपके logs में दिखेगा।
एक सीमा स्पष्ट खींचें: reverse-engineered schema बताता है कि दूसरा पक्ष अभी क्या लौटाता है, यह नहीं कि वह लौटाने का वादा क्या करता है। पहला monitoring के लिए है; केवल दूसरा contract या API दस्तावेज़ में जाना चाहिए।
7. पूरा होने के बाद, validation errors को विशिष्ट फ़ील्ड की ओर वापस इंगित कराएँ
Schema उपयोगी हो जाने के बाद अगली चिंता पठनीय errors है। ajv error object में पाँच फ़ील्ड होते हैं, और पहले चार उपयोगी हैं:
| फ़ील्ड | अर्थ | उदाहरण (id: 0 जो minimum: 1 का उल्लंघन कर रहा है) |
|---|---|---|
instancePath |
डेटा में गलत मान की ओर JSON Pointer | "/id" |
schemaPath |
schema में चले नियम की स्थिति | "#/properties/id/minimum" |
keyword |
विफल बाधा का नाम | "minimum" |
params |
उस बाधा का machine-readable विवरण | { "comparison": ">=", "limit": 1 } |
message |
अंग्रेज़ी गद्य जो versions के बीच बदलता है, इसे parse न करें | "must be >= 1" |
एक असममिति सबसे आसानी से फिसलती है: जब required विफल होता है, instancePath मूल object की ओर इशारा करता है (सामान्यतः खाली स्ट्रिंग, यानी root), गायब फ़ील्ड की ओर नहीं। गायब key का नाम params.missingProperty में है। इसी तरह, जब additionalProperties विफल होता है, अतिरिक्त key का नाम params.additionalProperty में है।
इसलिए errors को "field → message" में map करते समय पहले इन दो विशेष स्थितियों को संभालें:
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
/** Form highlighting के लिए ajv error array को { fieldPath: [message] } में बदलें */
function groupByField(errors) {
const grouped = {};
for (const error of errors) {
// required और additionalProperties के लिए गलत फ़ील्ड params में रहती है, instancePath में नहीं
const field =
error.keyword === "required"
? error.params.missingProperty
: error.keyword === "additionalProperties"
? error.params.additionalProperty
: error.instancePath.replace(/^\//u, "").split("/").join(".") ||
"(root)";
(grouped[field] ??= []).push(error.message);
}
return grouped;
}
if (!validate(data)) console.log(groupByField(validate.errors));
allErrors: true वैकल्पिक नहीं है। डिफ़ॉल्ट configuration में ajv पहली error पर रुक जाता है, इसलिए उपयोगकर्ता एक फ़ील्ड ठीक करता है, फिर submit करता है, अगला error मिलता है, और एक form भरने के लिए ही तीन round trips चाहिए। अगर आप कस्टम संदेशों के लिए ajv-errors भी इस्तेमाल करते हैं, तो इस flag के बिना plugin चुपचाप केवल कुछ संदेश बदलेगा।
8. दोबारा उपयोग योग्य rollout प्रक्रिया
ऊपर के चरणों को एक स्थिर दिनचर्या में पिरोएँ और हर नए API को integrate करते समय उसका पालन करें:
- Sample: 5–20 असली responses लें, जानबूझकर खाली फ़ील्ड, nulls, खाली arrays और अतिरिक्त फ़ील्ड वाली पंक्तियाँ चुनें;
- Infer: उन्हें एक साथ array के रूप में generator में चिपकाएँ, "all required" बंद करें, और
itemsलें; - Semantics पूरी करें: एक-एक करके
format,enum,minimum/maximum,minItemsऔरadditionalPropertiesजोड़ें; - स्व-जाँच: मूल नमूने 100% पास होने चाहिए, वरना चरण 1 पर लौटकर नमूने जोड़ें;
- Negative proof: हर बाधा के लिए एक "विफल होना चाहिए" input तैयार करें और पुष्टि करें कि वह सचमुच विफल होता है — केवल पास होने वाला रास्ता जाँचना न जाँचने के बराबर है;
- Commit: schema को version control में रखें, विफल नमूनों को test cases के रूप में रखें, और API कभी फ़ील्ड बदले तो पहले tests चलाएँ।
चरण 5 की negative testing सबसे आसानी से छूटती है। required में key जोड़कर कभी यह सत्यापित न करना कि गायब key error देती है, इस दाँव पर लगाना है कि validator आपके मानसिक मॉडल से सहमत है।
import { describe, expect, it } from "vitest";
describe("user schema", () => {
it("accepts a complete record", () => {
expect(validate({ id: 1, name: "Ada", tags: [], score: 10 })).toBe(true);
});
it("rejects a record without name", () => {
expect(validate({ id: 2, tags: [], score: 7 })).toBe(false);
// कारण पर भी assertion करें, ताकि test किसी और फ़ील्ड की विफलता से पास न हो जाए
expect(
validate.errors?.some((e) => e.params?.missingProperty === "name"),
).toBe(true);
});
});
वह अंतिम assertion महत्वपूर्ण है। केवल false पर assertion करना schema टूटने के बाद भी हरा रह सकता है — तब तक विफलता पूरी तरह अलग कारण से आ सकती है।
9. FAQ
क्या generator का बनाया schema सीधे प्रोडक्शन में जा सकता है
नहीं। यह केवल संरचना का snapshot है। आपको कम से कम formats, ranges, enums और unknown-field नीति जोड़नी होगी, और फिर मूल नमूनों तथा negative नमूनों दोनों को उससे गुज़ारना होगा। Generator "हाथ से properties टाइप करना उबाऊ है" हल करता है, "business नियम तय करना" नहीं।
Schema लेखन की keyword प्रणाली, $ref splitting और composition keywords के लिए JSON Schema: मूल बातों से प्रोडक्शन तक देखें।
सबसे अधिक फ़ील्ड वाला record अधिक डेटा क्यों अस्वीकार करता है
क्योंकि required "नमूने में क्या दिखा" से इकट्ठा होती है। पूर्ण-फ़ील्ड नमूना हर optional key को अनिवार्य बना देता है, जबकि असली डेटा में उनमें से अधिकांश keys अनुपस्थित होती हैं। कई records देना, या ऐसा record देना जो जानबूझकर फ़ील्ड छोड़ता है, आपको सबसे पूर्ण नमूने से सत्य के अधिक निकट ले जाता है।
क्या integer को integer के रूप में अनुमान लगाया जाएगा
इस साइट का generator ऐसा नहीं करता: integers और floats दोनों number के रूप में आउटपुट होते हैं, इसलिए {"price": 5} से बना schema 5.99 स्वीकार करता है। यह "नमूना संयोग से integer था, इसलिए प्रोडक्शन दशमलव वाली कीमतें अस्वीकार करता है" जैसी दुर्घटनाओं से बचाता है। कुछ third-party उपकरण मान से integer का अनुमान लगाते हैं, इसलिए money फ़ील्ड के साथ विशेष सावधानी रखें।
संख्या परिशुद्धता और type सीमाओं के विवरण के लिए छह JSON डेटा types और number precision के जाल देखें।
मैंने format जोड़ा पर कोई प्रभाव नहीं
draft 2019-09 से format डिफ़ॉल्ट रूप से केवल annotation है। या तो validator में format assertions स्पष्ट रूप से चालू करें (ajv को ajv-formats चाहिए), या कड़ी बाधा के लिए pattern इस्तेमाल करें। यह न मानें कि जोड़ने भर से कुछ रुक जाएगा।
required error बता रहा है, पर फ़ील्ड डेटा में स्पष्ट रूप से मौजूद है
पहले instancePath देखें। required error मूल object की ओर इशारा करता है, जिसका अर्थ है गायब key उस स्तर पर नहीं है — आम कारण हैं गलत nesting स्तर (/profile/name root पर name के समान नहीं है), अलग case (userId बनाम userID), या upstream serialization का undefined को पूरी तरह गिरा देना।
जाँच से पहले पुष्टि करें कि दस्तावेज़ स्वयं मान्य JSON है: comments, trailing commas और इसी तरह की समस्याएँ parsing चरण में ही विफल होती हैं, schema को मौका मिलने से पहले। JSON Parse और Format सीधे गलत पंक्ति की ओर इशारा करता है।
कितने records sample करूँ और कौन-सा draft इस्तेमाल करूँ?
रोज़मर्रा में 5–10 records sample करना सर्वोत्तम मूल्य देता है; required median 3 records पर converge होती है, और 12 records हर family कवर करते हैं। हर record सचमुच अलग स्थिति से आना चाहिए, और हर फ़ील्ड का हर रूप कम से कम एक बार दिखना चाहिए। Draft को 2020-12 पर पिन करें, क्योंकि OpenAPI 3.1 केवल वही version स्वीकार करता है।
ढीला additionalProperties डिफ़ॉल्ट अधिक खतरनाक क्यों है?
डिफ़ॉल्ट कोई keyword ही नहीं देता, इसलिए गलत लिखी key चुपचाप स्वीकार होती है, validation पास करती है, और फिर runtime पर विफल होती है — सीधे error से निदान कठिन। Config-शैली schemas को "additionalProperties": false स्पष्ट रूप से सेट करना चाहिए, जबकि API schemas को पहले compatibility लागत तौलनी चाहिए।
क्या reverse-engineered schema को कड़े gate के रूप में इस्तेमाल करूँ?
नहीं। Reverse-engineered schema बताता है कि दूसरा पक्ष अभी क्या लौटाता है, यह नहीं कि वह लौटाने का वादा क्या करता है, इसलिए यह monitoring और drift alerts में जाता है। Contract या API दस्तावेज़ में जो भी जाए, वह वादा किया गया contract ही होना चाहिए।