التحقق من مخطط API

التحقق من المخطط (Schema Validation) يفحص استجابة API مقابل مخطط: وصف قابل للقراءة آلياً للبنية التي يجب أن تكون عليها الاستجابة؛ أيّ الحقول موجودة وأيّها مطلوب، وما نوع كل قيمة، وما القيم والصيغ المسموحة، وهل يجوز ظهور حقول أخرى. وفي واجهات JSON يُكتب المخطط بلغة JSON Schema، ويوجد غالباً داخل مستند OpenAPI الخاص بالواجهة.

مثال ذلك أن مخطط طلب الشراء (Order) ينص على أن total رقم. فإذا أعادت الواجهة "total": "149.50"، أي سلسلة نصية، أبلغ المدقّق (Validator) عن مخالفة في الموضع $.total صادرة عن الكلمة المفتاحية type، مع أن أحداً لم يكتب تأكيداً (Assertion) لهذا الحقل. وبينما فحص التحقق من الاستجابة الحقولَ تأكيداً تلو آخر، يفحص المخطط البنية الكاملة لكل استجابة في خطوة واحدة.

لماذا يهم

تُبنى التطبيقات العميلة على البنية الموثّقة. فالحقل الذي يتغيّر نوعه، أو يختفي، أو يصبح null، أو يُعاد تسميته قد يُسقط تطبيق جوال أو يُفسد تقريراً بصمت بينما رمز الحالة ما زال 200 OK. ومثل هذه العيوب يسهل أن تفوت العين، وكثيراً ما تصيب نقاط نهاية عديدة دفعةً واحدة، كأن يحدث ذلك بعد تغيير في طريقة تحويل الخادم لكائناته إلى JSON، وهو ما يُسمّى التسلسل (Serialization).

وبالنسبة إلى المختبِر، المخطط مرجع جزئي للاختبار (Test Oracle)، أي مصدر للنتائج المتوقعة، يأتي مع وصف الواجهة. ففحص واحد يغطي عشرات الحقول، ويكشف الحقول غير الموثّقة التي قد تُسرّب بيانات داخلية، ويجعل كل تشغيل لاختبارات الانحدار فحصاً للبنية. كما أن مراجعة المخطط تفرض الأسئلة الصحيحة مبكراً: اختياري أم يقبل null؟ ما القيم المسموحة؟ هل يجوز للواجهة إضافة حقول؟

قراءة JSON Schema: الكلمات المفتاحية التي تؤدي معظم العمل

مخطط JSON Schema هو نفسه مستند JSON. كل كلمة مفتاحية فيه قاعدة يطبّقها المدقّق على الجزء المقابل من الاستجابة. وتحدّد $schema المسودة (Draft) التي يتبعها المخطط، والحالية هي ⁦2020-12⁩، لأن سلوك المدقّقات يختلف قليلاً بين المسودات.

يصف المخطط أدناه جسم الاستجابة في GET /orders/{orderId}، وهو مبني من الكلمات المفتاحية التي تستخدمها معظم المخططات:

  • type: أحد الأنواع object أو array أو string أو number أو integer أو boolean أو null. وقائمة مثل ["string", "null"] تسمح بأيٍّ منهما.
  • required: الحقول التي يجب أن توجد؛ والحقل المذكور في properties وحدها اختياري. وهي تفحص الوجود لا القيمة: فالحقل "couponCode": null موجود.
  • properties: قاعدة لكل حقل مسمّى، تُطبَّق عندما يكون الحقل موجوداً. وتؤدي items الدور نفسه لكل عنصر في المصفوفة.
  • enum وpattern وminimum وmaxLength وأمثالها: القيم المسموحة وأنماط النصوص والحدود. والمطابقة في enum تامة وتميّز حالة الأحرف. أما pattern فتعبير نمطي (Regular Expression): فالنمط ^ORD-[0-9]+$ يعني «يبدأ بـ ORD-، ثم رقم واحد أو أكثر، ثم ينتهي». ومن دون ^ و$ قد يطابق النمط أي جزء من النص، فتجتاز القيمة x-ORD-1-y الفحص.
  • format: صيغة مسمّاة مثل date-time أو email. ويجوز للمدقّقات أن تعاملها تعليقاً توضيحياً (Annotation) فقط، وهذا هو الافتراضي في المسودة ⁦2020-12⁩، فتأكّد أن مدقّقك يرفض القيمة الخاطئة فعلاً.
  • additionalProperties: هل يجوز ظهور حقول غير مذكورة في properties. فإذا غابت قُبل أي حقل إضافي، والقيمة false ترفض الحقول الإضافية.
مخطط طلب الشراء كلمةً مفتاحية تلو أخرى: required تطلب الوجود فقط، وadditionalProperties: false ترفض الحقول غير الموثّقة، ولا يُحتسب format إلا إذا كان المدقّق يفحصه.

مخطط JSON Schema لـ GET /orders/{orderId}

{
  الجزء 1:"$schema": "https://json-schema.org/draft/2020-12/schema",
  الجزء 2:"type": "object",
  الجزء 3:"required": ["id", "status", "total", "createdAt", "items"],
  الجزء 4:"properties": {
    "id": { "type": "string", الجزء 5:"pattern": "^ORD-[0-9]+$" },
    "status": { الجزء 6:"enum": ["pending", "paid", "shipped", "cancelled"] },
    "total": { "type": "number", "minimum": 0 },
    "createdAt": { "type": "string", الجزء 7:"format": "date-time" },
    "couponCode": { الجزء 8:"type": ["string", "null"] },
    "items": {
      "type": "array",
      الجزء 9:"minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "qty", "price"],
        "properties": {
          "sku": { "type": "string" },
          "qty": { "type": "integer", "minimum": 1 },
          "price": { "type": "number", "minimum": 0 }
        }
      }
    }
  },
  الجزء 10:"additionalProperties": false
}
  1. 1$schema: المسودة، وهي هنا ⁦2020-12⁩يختلف سلوك المدقّقات قليلاً بين المسودات
  2. 2type: يجب أن يكون الجسم كائن JSONتُرفض المصفوفة أو النص أو null في المستوى الأعلى
  3. 3required: هذه الحقول الخمسة يجب أن توجدالوجود فقط: الحقل الذي قيمته null موجود
  4. 4properties: قاعدة لكل حقل مسمّىتُطبَّق فقط عندما يكون الحقل موجوداً
  5. 5pattern: يجب أن يطابق id التعبير النمطيلا يُثبَّت على بداية النص ونهايته إلا بـ ^ و$
  6. 6enum: هذه القيم الأربع للحالة فقطمطابقة تامة تميّز حالة الأحرف: "Shipped" تفشل
  7. 7format: تاريخ ووقت وفق RFC 3339تعليق توضيحي افتراضياً في ⁦2020-12⁩: تأكّد أن مدقّقك يفحصه
  8. 8اختياري ويقبل null: الحقل couponCodeليس في required، وnull نوع مسموح
  9. 9minItems: بند واحد على الأقلمن دونها تجتاز القائمة الفارغة كل قاعدة items
  10. 10additionalProperties: لا حقول غير موثّقةمن دونها يُقبل أي حقل إضافي
النسخة النصية
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "status", "total", "createdAt", "items"],
  "properties": {
    "id": { "type": "string", "pattern": "^ORD-[0-9]+$" },
    "status": { "enum": ["pending", "paid", "shipped", "cancelled"] },
    "total": { "type": "number", "minimum": 0 },
    "createdAt": { "type": "string", "format": "date-time" },
    "couponCode": { "type": ["string", "null"] },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "qty", "price"],
        "properties": {
          "sku": { "type": "string" },
          "qty": { "type": "integer", "minimum": 1 },
          "price": { "type": "number", "minimum": 0 }
        }
      }
    }
  },
  "additionalProperties": false
}

جرّبها: مدقّق JSON Schema

أين يوجد المخطط: OpenAPI

يصف مستند OpenAPI، المكتوب بصيغة YAML أو JSON، واجهة HTTP كاملة: المسارات، والطرق، والمعاملات، وأجسام الطلبات، ولكل رمز حالة جسمَ الاستجابة لكل نوع وسائط (Media Type). وتوصف الأجسام بمخططات تُعرَّف غالباً مرة واحدة تحت components/schemas ويُعاد استخدامها بالإشارة $ref. ويختار المدقّق المخطط حسب المسار والطريقة ورمز الحالة ونوع الوسائط، فاستجابة 404 تُفحص مقابل مخطط الخطأ لا مخطط طلب الشراء.

والإصدار مهم. يستخدم الإصداران OpenAPI ⁦3.1⁩ و⁦3.2⁩ مخططات JSON Schema ⁦2020-12⁩، مع بضع كلمات مفتاحية خاصة بـ OpenAPI. أما OpenAPI ⁦3.0⁩ فيستخدم مجموعة جزئية موسّعة من مسودة أقدم: تقبل type فيه قيمة واحدة فقط، ويُكتب الحقل الذي يقبل null بالشكل nullable: true بدلاً من قائمة أنواع. فاقرأ سطر openapi في أعلى المستند قبل أن تثق بنتائج أي مدقّق.

openapi: 3.1.0
info: { title: Orders API, version: 1.0.0 }
paths:
  /orders/{orderId}:
    get:
      parameters:
        - { name: orderId, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          description: No order with this id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Order:
      type: object
      required: [id, status, total, createdAt, items]
      additionalProperties: false
      properties:
        id: { type: string, pattern: '^ORD-[0-9]+$' }
        status: { enum: [pending, paid, shipped, cancelled] }
        couponCode: { type: [string, 'null'] }
        # ...وبقية الخصائص كما في مخطط JSON Schema أعلاه تماماً
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }

جرّبها: عارض OpenAPI جرّبها: مدقّق OpenAPI

مثال تطبيقي: طلب شراء يعيد 200 OK

يستدعي المختبِر GET /orders/ORD-1042 فيتلقى 200 OK مع الجسم المعروض في آخر هذا القسم. فحص رمز الحالة ناجح، ولا شيء يبدو معطوباً للوهلة الأولى.

وعند التحقق من الجسم مقابل مخطط طلب الشراء تظهر خمس مخالفات، كلٌّ منها موضع في الاستجابة والكلمة المفتاحية التي رفضته، ونتيجة ناجحة تستحق الانتباه:

  • $.status بالكلمة enum: القيمة "Shipped" غير مسموحة. فالقيمة الموثّقة هي shipped، والعميل الذي يقارن النصوص مقارنةً تامة لن يتعرّف عليها.
  • $.total بالكلمة type: سلسلة نصية في موضع يتطلب رقماً. والعميل الذي يجمع المبالغ قد يلصق النصوص بعضها ببعض أو يتعطّل.
  • $.createdAt بالكلمة format: ليست تاريخاً ووقتاً وفق RFC 3339، إذ لا فاصل T ولا ثوانٍ ولا فرق توقيت. ولا تظهر إلا إذا كان المدقّق يفحص الصيغ.
  • $.items[0] بالكلمة required: الحقل price غائب عن بند الطلب.
  • $.internalNote بالكلمة additionalProperties: حقل لا يسمح به العقد، وهو هنا بيانات داخلية. وتُدرج قائمة OWASP API Security Top 10 لعام 2023 كشف مثل هذه الخصائص تحت البند API3، أي Broken Object Property Level Authorization.
  • أما couponCode فقيمته null ويجتاز الفحص: فهو اختياري ونوعه يسمح بالقيمة null.
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "ORD-1042",
  "status": "Shipped",
  "total": "149.50",
  "createdAt": "2026-09-14 10:32",
  "couponCode": null,
  "items": [
    { "sku": "BK-201", "qty": 2 }
  ],
  "internalNote": "VIP customer, call before delivery"
}

جرّبها: مدقّق مخططات API

ما لا يخبرك به التحقق من المخطط

يصف المخطط شكل البيانات الصالحة، لا صحتها. أصلِح كل المخالفات السابقة وقد تبقى الاستجابة خاطئة: فمع "total": 159.50 وبند واحد فيه قطعتان سعر كلٍّ منهما ⁦74.75⁩، ينجح الفحص مع أن مجموع البنود ⁦149.50⁩. فنجاح الفحص يعني فقط أن الاستجابة سليمة البنية مقارنةً بذلك الإصدار من المستند.

وتحتاج الأمور التالية إلى فحوص أخرى:

  • القيم والحسابات: المجاميع والأعداد، وهل id هو طلب الشراء الذي طلبته. فلا يستطيع JSON Schema مقارنة قيمتين أو إجراء حساب.
  • قواعد العمل والحالة: هل shipped حالة تالية صحيحة لهذا الطلب. تستطيع if/then التعبير عن شروط بسيطة، لكنها لا ترى ما مرّ به الطلب.
  • التفويض: هل يحق لهذا المستدعي أن يرى هذا الطلب أصلاً.
  • رموز الحالة والترويسات والزمن: مدقّق الجسم لا يراها، فتحقّق منها بتأكيدات منفصلة.
  • المصفوفات الفارغة: قائمة items الفارغة تجتاز أي قاعدة items، فنقطة النهاية التي تعيد [] لا تختبر مخطط العنصر أبداً. استخدم minItems حيث يضمن العقد وجود محتوى، وبيانات اختبار تُنتج قوائم غير فارغة.
  • المخطط نفسه: إذا كان المستند خاطئاً أو قديماً، وافقه المدقّق. والمخطط المولَّد من عيّنة واحدة يجعل عادةً كل حقل ظهر فيها مطلوباً وينسخ عيوب العيّنة؛ فراجعه مقابل المتطلبات أولاً.

جرّبها: مولّد JSON Schema

أخطاء شائعة

  • اعتبار نجاح الفحص بالمخطط دليلاً على أن «الاستجابة صحيحة». فهو يثبت البنية فقط، وتبقى القيم بحاجة إلى تأكيدات.
  • افتراض أن الحقل المذكور في properties إلزامي. وحدها required تجعله كذلك، وrequired تقبل null؛ فاستخدم type لمنع null.
  • إغفال additionalProperties فتفوتك الحقول المسرّبة، أو جعلها false في فحوص العميل دون اتفاق مع الفريق، فتكسر كل إضافة لحقل اختياري عملية البناء.
  • الثقة بـ format دون التأكد من أن المدقّق يفحصها: فقد تمرّ "email": "not-an-email" بصمت.
  • التحقق مقابل مخطط مولَّد من عيّنة واحدة، أو منسوخ في الاختبار قبل أشهر، بدلاً من مستند OpenAPI الحالي.
  • التحقق من استجابات النجاح وحدها. فلاستجابات الخطأ (400 و404 و422) مخططات أيضاً، والعملاء يحلّلونها.

من منظور ضمان الجودة

يبدأ العمل على المخطط قبل وجود أي استجابة. راجع مسودة مستند OpenAPI بوصفها أساساً للاختبار (Test Basis)، واسأل عن كل حقل: هل هو مطلوب؟ هل يمكن أن يكون null؟ ما القيم والصيغ المسموحة؟ ما حدوده؟ وهل الحقول الإضافية مقبولة؟ والغموض الذي تكتشفه هنا رخيص الإصلاح، ويصبح مادة لاختبارات الحدود والاختبارات السلبية في الدروس التالية.

وأثناء التنفيذ، أجرِ فحصاً بالمخطط على كل استجابة في اختبارات API لديك، وأضف تأكيدات للقيم التي لا يراها. وعندما يفشل الفحص، سجّل الطلب والموضع والكلمة المفتاحية والقاعدة المتوقعة وإصدار OpenAPI الذي استخدمته. ثم اسأل المطور أو مالك الواجهة أيهما الخاطئ: الواجهة أم المستند؛ فإصلاح الطرف الخطأ يُخفي العيب.

تمرين عملي

راجع مخطط منتج وتحقّق من استجابة

المطلوب

المتطلب: تعيد GET /products/{sku} الحقل sku (نص)، وname (من ⁦1⁩ إلى ⁦120⁩ حرفاً)، وprice بالريال (أكبر من ⁦0⁩)، وstock (عدد صحيح، ⁦0⁩ فأكثر)، وكلها موجودة دائماً، إضافةً إلى قائمة اختيارية tags من التسميات النصية. ولا يُسمح بأي حقول أخرى. كتب زميلك المخطط أدناه، وأعادت الواجهة الاستجابة التي تليه.

حدّد مواضع قصور المخطط عن المتطلب، ثم استنتج ما يبلّغ عنه كل إصدار من المخطط.

المادة
المخطط (مسودة الزميل):
{
  "type": "object",
  "required": ["sku", "name"],
  "properties": {
    "sku":   { "type": "string" },
    "name":  { "type": "string", "maxLength": 120 },
    "price": { "type": "number", "minimum": 0 },
    "stock": { "type": "number" },
    "tags":  { "type": "array", "items": { "type": "string" } }
  }
}

جسم الاستجابة:
{ "sku": "BK-201", "name": "", "stock": 2.5, "tags": ["new", 7], "costPrice": 41.2 }
الخطوات
  1. قارن المخطط بالمتطلب كلمةً مفتاحية تلو أخرى، واكتب كل فجوة مع إصلاحها.
  2. الصق المخطط والاستجابة في أداة JSON Schema Validator، ودوّن ما تبلّغ عنه.
  3. صحّح المخطط وتحقّق مرة أخرى، واكتب كل مخالفة مع موضعها وكلمتها المفتاحية.
  4. اذكر أمرين يقتضيهما المتطلب ولا يستطيع أي مخطط فحصهما.
إظهار الإجابة النموذجية
الإجابة النموذجية

الفجوات: يجب أن تضم required أيضاً price وstock؛ ويحتاج name إلى "minLength": 1؛ ويحتاج price إلى "exclusiveMinimum": 0 لأن "minimum": 0 تقبل الصفر؛ ويجب أن يكون stock من النوع "type": "integer" مع "minimum": 0؛ وينقص المخططَ "additionalProperties": false.

مخطط الزميل يبلّغ عن مخالفة واحدة: $.tags[1] بالكلمة type (فالقيمة ⁦7⁩ ليست نصاً). أما الاسم الفارغ، والسعر الغائب، والمخزون الكسري، والحقل الإضافي costPrice فتجتاز كلها.

والمخطط المصحّح يبلّغ عن خمس: $ بالكلمة required (لا سعر)، و$.name بالكلمة minLength، و$.stock بالكلمة type، و$.tags[1] بالكلمة type، و$.costPrice بالكلمة additionalProperties. والأخيرة تستحق تقرير خطأ مستقلاً، فسعر التكلفة بيانات تجارية داخلية.

لا يستطيع أي مخطط أن يتحقق من أن sku المُعاد هو المطلوب، ولا من أن price وstock يطابقان الكتالوج والمستودع. فهذه تحتاج إلى تأكيدات للقيم مقابل مصدر حقيقة معروف.

أسئلة شائعة

ما الفرق بين JSON Schema وOpenAPI؟

يصف JSON Schema قيمة JSON واحدة: حقولها وأنواعها وصيغها وحدودها. أما OpenAPI فيصف واجهة HTTP كاملة، من مسارات وطرق ومعاملات ورموز حالة وترويسات وأمان، ويستخدم المخططات لوصف الأجسام. ومخططات OpenAPI ⁦3.1⁩ و⁦3.2⁩ هي JSON Schema ⁦2020-12⁩ مع بضع كلمات مفتاحية خاصة بـ OpenAPI، بينما يستخدم OpenAPI ⁦3.0⁩ مجموعة جزئية موسّعة من مسودة أقدم.

هل التحقق من المخطط هو نفسه اختبار العقد؟

هو جزء منه. ففحص الاستجابات مقابل مستند OpenAPI الخاص بالمزوّد هو اختبار عقد قائم على المواصفة، وبعض الفرق تسمّي أي فحص بالمخطط اختبار عقد. ويشمل اختبار العقد (Contract Testing) أيضاً العقود التي يقودها المستهلك، ويشرحها درس لاحق.

هل يجب أن تضبط مخططات الاستجابة additionalProperties على false؟

في اختبارات المزوّد نفسه يستحق ذلك غالباً، لأنه يكشف الحقول غير الموثّقة والمسرّبة. أما العميل فالأفضل عادةً أن يتسامح مع الحقول الجديدة، وإلا كسرته كل إضافة. اتفق على القاعدة مع المطورين وسجّلها في نهج الاختبار لديكم، حتى يعني الفشل الشيء نفسه للجميع.

هل يستطيع المخطط فحص قواعد بين الحقول؟

جزئياً. تستطيع dependentRequired أن تقول «إذا وُجد refundId فيجب أن يوجد refundedAt أيضاً»، وتستطيع if/then/else أن تقول «إذا كانت status هي cancelled فإن cancelledAt مطلوب». لكن المخطط لا يستطيع مقارنة قيمتين أو إجراء حساب، لذا تبقى قواعد مثل «المجموع يساوي مجموع البنود» أو «تاريخ النهاية بعد تاريخ البداية» تأكيداتٍ للقيم.

المراجع

  • JSON Schema SpecificationJSON Schemaالمفردات المستخدمة لوصف بنية مستندات JSON والتحقق منها (الأنواع والحقول المطلوبة والصيغ).
  • OpenAPI SpecificationOpenAPI Initiativeالمعيار لوصف واجهات REST: المسارات والعمليات والمعاملات والمخططات والاستجابات وأنظمة الأمان.
  • ISTQB GlossaryISTQBالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
  • OWASP API Security Top 10 (2023)OWASP Foundation · 2023أكثر عشر نقاط ضعف شيوعاً في واجهات API: خلل التفويض على مستوى الكائن، وخلل التوثيق، والاستهلاك غير المقيد للموارد، وغيرها.

تدرّب بأدوات متقن

التحقق من مستند JSON مقابل مخطط JSON Schema (الكلمات المفتاحية الأساسية في draft-07 / 2019-09 / 2020-12).

JSON والبيانات

تحقّق من استجابات API وفق JSON Schema أو مخطط استجابة OpenAPI.

اختبار واجهات API

عرض مواصفة OpenAPI 3.x / Swagger 2.0 (بصيغة JSON أو YAML) كتوثيق قابل للتصفح لنقاط النهاية.

الويب

التحقق من مستندات OpenAPI 3.x وSwagger 2.0 للكشف عن الأخطاء البنيوية ومشكلات أفضل الممارسات.

الويب

توليد مخطط JSON Schema تلقائياً من مستند JSON نموذجي.

JSON والبيانات

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة