لماذا يظل JSON Schema المولّد تلقائيًا يرفض البيانات الحقيقية
الخلاصة: المخططات المولّدة كثيرًا ما تمنع بيانات صالحة. تعلّم لماذا تنتج العينات الأغنى مخططات أسوأ، ثم سير العمل: أخذ عينات متعددة السجلات، والاستكمال الدلالي، وتحديد الأخطاء بدقة.

الخلاصة: حدّد حدود المطلوب/الاختياري يدويًا أولًا — غذِّ المولّد 5–20 سجلًا متعدد الأشكال مع تعطيل "الكل مطلوب" — ثم ولّد المخطط وأضف format وenum وadditionalProperties يدويًا؛ فالعينة الواحدة كاملة الحقول ستكون دائمًا مفرطة الصرامة.
لا يرى المولّد إلا السجل الواحد الذي لصقته. فهو يقرأ "هذا السجل فيه بريد إلكتروني" على أنه "كل سجل يجب أن يحتوي بريدًا"، فكلما صارت عيناتك أكمل، صار مطالبها أكثر.
1. المولّد يقوم بربط بنيوي، لا بفهم تجاري
سلّم قطعة JSON إلى مولّد وسيفعل شيئًا واحدًا بالضبط: يمشي على بنية القيم ويترجم كل عقدة إلى كلمات مفتاحية في المخطط. وجدول الربط ثابت بالكامل:
| القيمة في العينة | النوع المستنتج | ملاحظات |
|---|---|---|
"hello" |
{"type": "string"} |
لا يميز البريد أو التاريخ أو UUID |
42 |
{"type": "number"} |
لا يُستنتج العدد الصحيح على صورة integer |
3.14 |
{"type": "number"} |
كما سبق |
true |
{"type": "boolean"} |
— |
null |
{"type": "null"} |
يتعرف على null فقط، ولا يتعرف على "قابلية null" أبدًا |
[1, "a"] |
{"type": "array", "items": {"type": ["number", "string"]}} |
تُجمع أنواع العناصر في اتحاد |
{"a": 1} |
{"type": "object", "properties": {...}, "required": [...]} |
وrequired هي جوهر المشكلة هنا |
كل المتاعب في الصف الأخير. فـrequired لا تُشتق من الأنواع؛ بل من "وجود هذا المفتاح في هذه العينة". وعندما تكون العينة سجلًا واحدًا، فهذا القول يكافئ "كل مفتاح إلزامي".
والبيانات التي تعيدها واجهات API الحقيقية تحتوي دائمًا تقريبًا على نصف حقولها اختياريًا. وهنا يبدأ التعارض.
2. خمسة سجلات متشابهة الشكل، وخمسة معدلات قبول
لنبدأ بالقياسات. أي شخص ينظر إلى السجلات الخمسة أدناه سيقول "هذه بالشكل نفسه":
[
{
"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 في 3 من السجلات الخمسة، ويظهر admin مرة واحدة فقط، والسجل 2 يحتوي مصفوفة tags فارغة.
الآن ولّد مخططًا من كل سجل على حدة، ثم استخدم كل نتيجة للتحقق من السجلات الخمسة كلها (المدقق: ajv 8):
| مولّد من | عدد المفاتيح | required المولّدة |
يقبل من 5 |
|---|---|---|---|
| السجل 1 | 5 | id, name, email, tags, score |
3 سجلات |
| السجل 2 | 4 | id, name, tags, score |
5 سجلات |
| السجل 3 | 5 | id, name, email, tags, score |
3 سجلات |
| السجل 4 | 5 | id, name, email, tags, score |
سجل واحد |
| السجل 5 | 6 | id, name, email, tags, score, admin |
سجل واحد |
لا شيء من المخططات الخمسة صحيح، لكن الوحيد الذي يقبل كل البيانات هو المولّد من السجل 2 — السجل الذي يفتقد email. وهو يفوز لسبب واضح: المفاتيح التي لا تظهر أبدًا لا تُكتب في required.
والسجل 4 أعظم فائدة: فقيمة email فيه null، فيثبّت المولّد {"type": "null"}، فتُرفض السجلات الأربعة الأخرى ذات البريد النصي كلها.
3. لماذا يكون "لصق السجل الأكمل" أسوأ خيار
الحدس يقول "الصق العينة ذات الحقول الأكثر فيكتمل المخطط". والقياسات تُظهر العكس تمامًا — وقد أجرى طرف ثالث اختبارًا محكومًا أوسع.
استخدم absurdtools 3000 "عائلة بيانات" عشوائية، 8 سجلات لكل عائلة، مع حضور كل حقل اختياري نحو نصف الوقت، وقاس مقدار البيانات التي يستطيع مخطط مولّد من عينات بأعداد مفاتيح مختلفة قبولها:
| عدد مفاتيح العينة | نسبة بيانات العائلة المقبولة |
|---|---|
| 2 | 100.0% |
| 3 | 56.8% |
| 4 | 34.8% |
| 5 | 23.4% |
| 6 | 17.7% |
| 7 | 14.9% |
والخلاصة ليست "تراجعًا طفيفًا" بل انعكاسًا كاملًا: أنتجت العينة الأدنى المخطط الأكثر تساهلًا في 95% من العائلات، بينما لم يحقق ذلك السجل الأكمل إلا في 0.1% من الحالات.
والسبب في دلالات required، التي تعني "الكل"؛ فلا يمكن ملاحظة الاختيارية إلا بالغياب. فلصق سجل فيه كل حقل يقول للمولّد "لا يجوز إغفال أي من هذه المفاتيح"؛ ولصق سجل يفتقد حقلًا يمنحه الدليل على أن "هذا المفتاح قد يغيب".
وتذكّرها بجملة واحدة: قيمة العينة ليست اكتمالها، بل ما تغطيه من غيابات.
كم عدد السجلات الكافي
قاس التجارب ذاتها سرعة التقارب أيضًا: فبتراكم السجلات واحدًا واحدًا وفق قاعدة "يبقى المفتاح مطلوبًا فقط إذا احتوى عليه كل سجل حتى الآن"، فإن متوسط عدد السجلات التي تحتاجها required لتتقارب إلى المجموعة المطلوبة الحقيقية هو 3؛ وتغطي 6 سجلات نحو تسعين بالمئة من العائلات، وتغطي 12 جميعها.
لذا فاختيار 5–10 سجلات يوميًا هو أفضل قيمة مقابل الجهد؛ وإجبار عشرين سجلًا أو أكثر يعطي فائدة إضافية ضئيلة جدًا.
لكن ثمة شرط مسبق: يجب أن تأتي هذه السجلات من مواقف مختلفة فعلًا. فخمسة سجلات منسوخة من القالب نفسه تتوقف عن تقديم أي معلومة جديدة من الثاني فصاعدًا — فمجموعات حقولها متطابقة، ولا يمكن لـrequired أن تتقلص بمفتاح واحد.
4. الإصلاح: غذِّ عدة سجلات معًا ودع الغيابات تتكلم
بما أن أخذ العينة من سجل واحد يكون مفرط الصرامة بالضرورة، فالنهج الصحيح هو أن ينظر المولّد إلى عدة سجلات معًا وتتقارب required بقاعدة "مطلوب فقط إذا احتوى عليه كل سجل".
المولّد في هذا الموقع يقبل مصفوفة مباشرة، وفيه مفتاح "الكل مطلوب". أطفئه فيقرر حسب عدد مرات الظهور: فقط المفاتيح التي تحملها كل السجلات تُكتب في required.
وما زلنا مع السجلات الخمسة نفسها — لكن هذه المرة ادمجها في مصفوفة واستنتج معًا، ثم خُذ items كعقد العنصر:
| السجلات المُغذّاة | required المستنتجة |
التحقق من الخمسة كلها |
|---|---|---|
| سجل واحد | id, name, email, tags, score |
3 تنجح |
| سجلان | id, name, tags, score |
4 تنجح |
| 3 سجلات | id, name, tags, score |
4 تنجح |
| 4 سجلات | id, name, tags, score |
5 تنجح |
| 5 سجلات | id, name, tags, score |
5 تنجح |
تتقارب required بمجرد إضافة السجل الرابع، ولا تتغير بعده. ويوضع email وadmin بشكل صحيح في properties لا في required. ولأن بريد السجل 4 هو null، فإن الاستنتاج متعدد السجلات يدمجه في {"type": ["string", "null"]}، فيمر كل من السلاسل وnull — وهو ما لا يستطيعه أخذ العينة من سجل واحد أبدًا.
وهذا من رتبة النتيجة نفسها التي وجدها absurdtools: "تتقارب required بعد متوسط 3 سجلات".
غير أن للاستنتاج متعدد السجلات نقاط عمياء خاصة به: فإذا كان email في هذه السجلات null في كل واحد منها (ولنقل إنها كلها من حسابات لم تُكمل ملفاتها الشخصية)، فتصبح نتيجة الاستنتاج {"type": "null"}، وتظل رسائل البريد النصية مرفوضة. وبالمثل، إذا كانت كل قيمة tags مصفوفة فارغة، فلا يمكن استنتاج نوع العنصر إطلاقًا.
لذا فلأخذ العينات شرط صارم آخر: يجب أن تظهر كل صورة من صور كل حقل مرة واحدة على الأقل. اجلب سلسلة واحدة ورقمًا واحدًا وnull واحدًا ومصفوفة فارغة واحدة؛ ولا تدع أي صورة تغيب كليًا.
وبشكل ملموس:
- اسحب 5–20 استجابة من السجلات أو المحاكيات أو الطلبات الحقيقية، واحرص على تضمين الصفوف الشاذة (الصف ذو الحقول الفارغة، والصف ذو null، والصف ذو المصفوفة الفارغة)؛
- اجمعها في مصفوفة JSON وألصقها في المولّد؛
- أطفئ "الكل مطلوب" واختر draft 2020-12؛
- النتيجة
type: arrayمعitems؛ وخُذ قسمitemsكعقد للسجل الواحد؛ - تحقق فورًا من العينات الأصلية بهذا المخطط — يجب أن تنجح 100%. فإن فشل واحد فقط، فعينتك غير كافية؛ ارجع وأضف سجلات.
والخطوة 5 أرخص فحص ذاتي. فإذا لم يستطع المولّد استيعاب البيانات التي غذيته بها، فطريقة أخذ العينات لديك خاطئة، ولا فائدة من المضي قدمًا.
5. أربعة أشياء لا يملؤها المولّد — عليك كتابتها يدويًا
حتى بعد تغذية عدة سجلات، تظل الأمور التالية غير قابلة للاستنتاج ويجب إضافتها يدويًا:
| الجزء المفقود | السلوك المقاس | النتيجة | كيف تصلحه |
|---|---|---|---|
| تنسيق السلاسل | "a@b.com" ينتج {"type": "string"} فقط |
التحقق من "not an email" ينجح |
أضف "format": "email" وفعّل تأكيدات التنسيق |
| النطاقات والتعدادات | لا يرى أن score محصورة بين 0–100، ولا أن status له ثلاث قيم فقط |
تمر الأرقام السالبة والقيم الضخمة وقيم التعداد العشوائية كلها | أضف minimum / maximum / enum |
| سياسة الحقول غير المعروفة | لا تُولَّد additionalProperties |
يُقبل المفتاح المكتوب خطأً بصمت، وهو أخطر من الخطأ الصريح | أضف "additionalProperties": false لملفات نمط التهيئة |
| دلالات null والمجموعات الفارغة | عينة واحدة تحتوي null تثبّت {"type": "null"}؛ والمصفوفة الفارغة تنتج "items": {} |
تُرفض القيم الصالحة، أو تصبح عناصر المصفوفة غير مقيّدة إطلاقًا | غيّر إلى ["string", "null"]، وأعط المصفوفات الفارغة عناصر حقيقية |
ويستحق الصف الثالث ذكرًا منفصلًا: فالقيمة الافتراضية لـ additionalProperties هي "السماح". أي أن ملف تهيئة يكتب timeout على صورة timeuot يجتاز التحقق ثم لا يفعل شيئًا بصمت في وقت التشغيل. وبالنسبة إلى مخططات نمط التهيئة، فهذا بند يجب إغلاقه صراحةً.
ولـformat فخ أيضًا: فمنذ draft 2019-09 تنتمي format إلى مفردات التعليق، ولا تعلّق افتراضيًا بدلًا من أن تفرض. فإن أضفت format: "email" وظلت القيم غير الصالحة تمر، فالمرجح في ثماني حالات من عشر أن المدقق لم يفعّل validateFormats أو إضافة التنسيق المقابلة — وليس أن المخطط خاطئ.
وملاحظة عن اختيار الإصدار هنا: إذا كان المخطط سيدخل OpenAPI 3.1، فيجب أن يكون draft 2020-12، لأن 3.1 يضمّن ذلك الإصدار مباشرةً؛ أما سلاسل الأدوات الأقدم (نظام Swagger 2.0 وبعض تطبيقات $ref) فما زالت تتمركز حول draft-07. ويختلف سلوك الاثنين في الكلمات المفتاحية الشقيقة لـ $ref وفي الكلمات المتعلقة بالمصفوفات، فلا تخلطهما داخل مجموعة مخططات واحدة.
6. لواجهات API الخارجية غير الموثقة، اعكس العقد من عينات الاستجابة
دمج رد استدعاء خارجي، أو العمل مع شريك لا يكتب توثيقًا، أو صيانة نظام قديم عمره عشر سنوات — كل هذه تعني "البيانات في اليد، والمواصفة ليست كذلك". وهنا لا يكون المولّد مجرد راحة؛ بل أسرع طريق إلى عقد.
والطريقة هي نفسها أعلاه، مع أمرين إضافيين واجبين:
أولًا: خُذ عينات عبر الزمن وعبر الحالات. فحقول الطرف الثالث تتغير غالبًا مع حالة العمل — فالعرض الترويجي يضيف discount، وطلب الاسترداد يضيف refundAt، واستجابة الخطأ لا تحتوي data إطلاقًا. والتقاط لقطة واحدة يعني رؤية حالة واحدة.
وغذِّ نجاحات قليلة وإخفاقات قليلة ونتائج فارغة قليلة معًا: فـerror وdata غالبًا يظهران على سبيل التبادل، والاستنتاج متعدد السجلات يضع كليهما في properties ويتركهما خارج required — وهو مطابق للواقع تمامًا.
ثانيًا: احفظ المخطط في المستودع وأرفق فحصًا يوميًا. تحقق من استجابات اليوم الحقيقية مقابله، وستعرف بالحقول التي تختفي بصمت أو تغيّر نوعها قبل مستخدميك.
// شغّله مرة يوميًا: تنبيه انحراف العقد
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,
);
}
وقيمة هذه الممارسة ليست "توفير جهد كتابته يدويًا"، بل تحويل الاتفاق اللفظي إلى تنبيه قابل للتنفيذ. فالطرف الثالث لا يخبرك مسبقًا أنه غيّر حقلًا، لكن التغيير سيظهر بالتأكيد في سجلاتك.
وثمة حد يجب رسمه بوضوح: المخطط المعكوس يصف ما يعيده الطرف الآخر الآن، لا ما يتعهد بإعادته. الأول للمراقبة؛ والثاني وحده ينتمي إلى عقد أو توثيق API.
7. بعد الاكتمال، اجعل أخطاء التحقق تشير إلى حقول محددة
بعد أن يصبح المخطط صالحًا للاستخدام، يكون الشاغل التالي هو أخطاء قابلة للقراءة. ولكائن خطأ ajv خمسة حقول، الأربعة الأولى هي المفيدة:
| الحقل | المعنى | المثال (id: 0 ينتهك minimum: 1) |
|---|---|---|
instancePath |
JSON Pointer إلى القيمة المخالفة في البيانات | "/id" |
schemaPath |
موقع القاعدة المُطلقة في المخطط | "#/properties/id/minimum" |
keyword |
اسم القيد الفاشل | "minimum" |
params |
تفاصيل ذلك القيد القابلة للقراءة آليًا | { "comparison": ">=", "limit": 1 } |
message |
نص إنجليزي يتغير بين الإصدارات، لا تحلله | "must be >= 1" |
وثمة عدم تماثل أسهل ما يتعثر به: عندما تفشل required، يشير instancePath إلى الكائن الأب (عادةً السلسلة الفارغة، أي الجذر)، لا إلى الحقل المفقود. واسم المفتاح المفقود في params.missingProperty. وبالمثل، عندما تفشل additionalProperties، فاسم المفتاح الزائد في params.additionalProperty.
لذا فعند ربط الأخطاء بـ"الحقل → الرسالة"، تعامل مع هاتين الحالتين الخاصتين أولًا:
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
/** يحوّل مصفوفة أخطاء ajv إلى { 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 ليس اختياريًا. ففي التهيئة الافتراضية يتوقف ajv عند أول خطأ، فيصلح المستخدم حقلًا، ويعيد الإرسال، ويحصل على الخطأ التالي، ويحتاج ثلاث رحلات ذهاب وعودة لمجرد ملء نموذج واحد. وإن كنت تستخدم ajv-errors لرسائل مخصصة، فبدون هذا العلم ستستبدل الإضافة بعض الرسائل فقط بصمت.
8. عملية طرح قابلة لإعادة الاستخدام
حوّل الخطوات أعلاه إلى روتين ثابت واتّبعه في كل مرة تدمج فيها API جديدًا:
- خُذ عينة: اجلب 5–20 استجابة حقيقية، واختر عمدًا تلك ذات الحقول الفارغة وnull والمصفوفات الفارغة والحقول الإضافية؛
- استنتج: ألصقها معًا كمصفوفة في المولّد، وأطفئ "الكل مطلوب"، وخُذ
items؛ - أكمل الدلالات: أضف
formatوenumوminimum/maximumوminItemsوadditionalPropertiesبندًا بندًا؛ - افحص ذاتيًا: يجب أن تنجح العينات الأصلية 100%، وإلا فارجع إلى الخطوة 1 وأضف عينات؛
- إثبات سلبي: جهّز مُدخلًا "يجب أن يفشل" لكل قيد وتأكد أنه يفشل فعلًا — فالاختبار بمسار النجاح فقط مثل عدم الاختبار؛
- أودِع: ضع المخطط تحت التحكم بالإصدارات، واحفظ العينات الفاشلة كحالات اختبار، وشغّل الاختبارات أولًا كلما غيّر API حقلًا.
والخطوة 5، أي الاختبار السلبي، أسهل ما يُتخطّى. فإضافة مفتاح إلى required دون التحقق من أن مفتاحًا مفقودًا يرفع خطأً تعني المراهنة على أن المدقق يوافق نموذجك الذهني.
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);
// أكّد السبب أيضًا، حتى لا ينجح الاختبار بسبب فشل حقل آخر
expect(
validate.errors?.some((e) => e.params?.missingProperty === "name"),
).toBe(true);
});
});
وهذا التأكيد الأخير مهم. فتأكيد false فقط قد يظل ناجحًا حتى بعد كسر المخطط — فقد يأتي الفشل حينها من سبب مختلف تمامًا.
9. الأسئلة الشائعة
هل يستطيع المخطط الذي ينتجه المولّد الانتقال مباشرة إلى الإنتاج
لا. فهو مجرد لقطة للبنية. يجب على الأقل أن تضيف التنسيقات والنطاقات والتعدادات وسياسة الحقول غير المعروفة، ثم تمرّر العينات الأصلية والعينات السلبية معًا عبره. فالمولّد يحل "كتابة properties يدويًا مملة"، لا "هو الذي يقرر القواعد التجارية".
ولمعرفة نظام الكلمات المفتاحية في تأليف المخططات، وتقسيم $ref، وكلمات التركيب المفتاحية، انظر JSON Schema: من الأساسيات إلى الإنتاج.
لماذا يرفض السجل الأكثر حقولًا بيانات أكثر
لأن required تُجمع من "ما ظهر في العينة". فالعينة كاملة الحقول تحوّل كل مفتاح اختياري إلى إلزامي، بينما تغيب معظم تلك المفاتيح في البيانات الحقيقية. وتغذية عدة سجلات، أو سجل يغفل حقولًا عن قصد، تقرّبك إلى الحقيقة أكثر من العينة الأكمل.
هل يُستنتج العدد الصحيح على صورة integer
لا يفعل المولّد في هذا الموقع ذلك: فالأعداد الصحيحة والعشرية تُخرَج number، لذا فمخطط مولّد من {"price": 5} يقبل 5.99. وهذا يتجنب حوادث مثل "كانت العينة عددًا صحيحًا، فيرفض الإنتاج الأسعار العشرية". وبعض أدوات الطرف الثالث تستنتج integer من القيمة، فكن حذرًا بشكل خاص مع حقول المال.
وللتفاصيل عن دقة الأرقام وحدود الأنواع، انظر أنواع بيانات JSON الستة ومزالق دقة الأرقام.
أضفت format ولم يكن له أثر
منذ draft 2019-09 تكون format تعليقًا فقط افتراضيًا. إمّا أن تفعّل تأكيدات التنسيق صراحةً في المدقق (ويحتاج ajv إلى ajv-formats)، أو تستخدم pattern لقيد صارم. لا تفترض أن إضافتها ستمنع شيئًا.
required يبلّغ عن خطأ، لكن الحقل موجود بوضوح في البيانات
انظر إلى instancePath أولًا. فخطأ required يشير إلى الكائن الأب، وهذا يعني أن المفتاح المفقود ليس في ذلك المستوى — والأسباب الشائعة مستوى تداخل خاطئ (/profile/name ليس مثل name عند الجذر)، أو اختلاف حالة الأحرف (userId مقابل userID)، أو أن التسلسل في المنبع يُسقط undefined بالكامل.
وقبل التحقيق، تأكد أن المستند نفسه JSON صالح: فالتعليقات والفواصل اللاحقة الزائدة وما شابهها تفشل في مرحلة التحليل، قبل أن تصل إلى المخطط أصلًا. وتحليل JSON وتنسيقه يشير مباشرة إلى السطر المخالف.
كم سجلًا ينبغي أن آخذ، وأي إصدار أستخدم؟
أخذ 5–10 سجلات يوميًا يعطي أفضل قيمة؛ وتتقارب required عند متوسط 3 سجلات، وتغطي 12 سجلًا كل عائلة. ويجب أن يأتي كل سجل من موقف مختلف فعلًا، وأن تظهر كل صورة من صور كل حقل مرة واحدة على الأقل. وثبّت الإصدار على 2020-12، لأن OpenAPI 3.1 لا يقبل إلا ذلك الإصدار.
لماذا تكون القيمة الافتراضية المتساهلة لـ additionalProperties أخطر؟
لا تُصدر القيمة الافتراضية أي كلمة مفتاحية إطلاقًا، فيُقبل المفتاح المكتوب خطأً بصمت، ويجتاز التحقق، ثم يفشل في وقت التشغيل — وهو أصعب تشخيصًا من خطأ مباشر. ويجب أن تضبط مخططات نمط التهيئة "additionalProperties": false صراحةً، بينما تزن مخططات API تكلفة التوافق أولًا.
هل أستخدم مخططًا معكوسًا كبوابة صارمة؟
لا. فالمخطط المعكوس يصف ما يعيده الطرف الآخر الآن، لا ما يتعهد بإعادته، لذا فهو ينتمي إلى المراقبة وتنبيهات الانحراف. وأما ما يدخل عقدًا أو توثيق API فيجب أن يكون العقد المتعهد به.