JSON Toolkit

اختبارات العقود مع JSON Schema: لحام قواعد التحقق في CI

الخلاصة: المخطط لا ينتهي عندما يُكتب — هناك يبدأ التدهور. خطأ مطبعي واحد يُرخي التحقق بصمت، وإعادة هيكلة واحدة تكسر جميع العينات. يشرح هذا الدليل منظومة اختبارات عقود قابلة للتنفيذ: عينات إيجابية وسلبية ولقطات حالة، وفحص lint للمخطط نفسه، وتجميع لمرة واحدة مع إعادة استخدام، وأربع بوابات CI تحوّل التغييرات الكاسرة للعقود إلى بنيات حمراء.

متوسطقراءة نحو 8 دقيقةنُشر في 2026-11-05حُدِّث في 2026-11-05
مخطط اختبارات العقود مع JSON Schema وبوابات التحقق في CI

الخلاصة: المخطط كود، والكود يحتاج اختبارات. العينات الإيجابية تثبت أنه يمرّر ما ينبغي؛ والسلبية تثبت أنه يصد ما يجب أن يصدّه؛ ولقطات الحالة تراقب التغييرات غير المقصودة. ثم إن ajv compile ولقطات الحالة بوضع --ci يحرسان أثناء الليل في خطّك.

حين تنكسر قواعد التحقق لا يرمي أحد استثناءً: اتساع القبول لا ينتبه له أحد، وتشديد الرفض يدفعه الفريق كله. المخطط ليس ملف إعدادات — إنه كود لا يختبِره أحد.

1. المخطط ينكسر — من يعرف أولًا؟

لنبدأ بالفخ الذي وطئناه فعلًا. صِرّح بمخطط وفق مسودة 2020-12 وجمّعه بحزمة Ajv الافتراضية: يقفز الاستثناء فورًا:

Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"

نقطة الدخول الافتراضية في Ajv 8 لا تعرف سوى draft-07؛ ولأجل 2020-12 عليك الاستيراد من ajv/dist/2020. وفخ آخر: في الوضع الصارم، كتابة format: "email" دون تثبيت ajv-formats تُسقط التجميع:

Error: unknown format "email" ignored in schema at path "#/properties/email"

رؤية هذه الأخطاء محليًا في أول تجميع أمر جيد. المشكلة: كثير من المشاريع تؤجل التجميع حتى تصل طلبية، والمخطط المكسور ينتظر ضحيته الأولى. والأدهى هو المخطط نصف المكسور: required مكتوبة خطأً كسلسلة بدل المصفوفة ترمي schema is invalid: data/required must be array عند التجميع — لكن إن لم تمضِ تلك الخطوة في CI يومًا، فقد بقت مستلقية في الإنتاج.

لذا فالبوابة الأولى لا تتحقق من البيانات: إنها تتحقق من المخطط نفسه. أمر واحد:

node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
  const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
  ajv.compile(schema);
  console.log(`ok ${file}`);
}

لماذا ليس npx ajv compile: حزمة ajv بالإصدار 8 لا تحوي CLI، لذا يفشل npx ajv بـ could not determine executable to run؛ الـ CLI موجودة في الحزمة المنفصلة ajv-cli (5.0.0 في قياسنا). لكن ajv-cli 5 يدعم draft-07 وdraft-2019-09 فقط — وحين يُوجَّه إلى مخطط 2020-12 من القسم الأول يرمي تمامًا no schema with key or ref "https://json-schema.org/draft/2020-12/schema". لذلك يفحص مشروع 2020-12 عبر Ajv2020 نفسه الوارد في المقال، وnpx ajv-cli compile -s '**/*.schema.json' --strict يصلح لمشاريع draft-07 وحدها.

شغّله كما تشغّل أداة lint: الأخطاء المطبعية والكلمات المفتاحية المتقادمة والصيغ غير المسجلة تظهر منذ مرحلة PR. تلك أولى وجوه التشابه بين اختبار المخططات واختبار الكود العادي: تأكد أولًا أنه يُجمَّع.

2. الثلاثية: إيجابية وسلبية ولقطة حالة

هيكل اختبارات المخطط ثلاث قطع.

العينات الإيجابية: نماذج ملتقطة من ردود حقيقية ينبغي أن تجتاز. خذها من staging لا من الذاكرة — الإيجابيات المكتوبة يدويًا لا تغطي سوى المسار السعيد وتنسى null والسلاسل الفارغة وحدود الكائن المكوَّن من الحقول الإلزامية فقط.

العينات السلبية: لكل قيد في المخطط، إدخال مخالف واحد على الأقل. لـ minimum: 0 الزوج -1 (مرفوض) و0 (مقبول)؛ ولـ enum قيمة خارج القائمة؛ ولـ additionalProperties: false حقل زائد. السلبية أثقل من الإيجابية: المخطط الذي يمرّر بيانات سيئة بصمت هو خطأ صامت يلوّث حتى قاعدة البيانات.

اصطلاح تسمية يجعل اسم الملف تأكيدًا، قابل للنسخ من المصادر المفتوحة:

schemas/order.schema.json
fixtures/order/valid-minimal.json        # يجب أن تجتاز
fixtures/order/valid-full.json           # يجب أن تجتاز
fixtures/order/invalid-type.json         # يجب أن تُرفض
fixtures/order/invalid-extra-field.json  # يجب أن تُرفض

وحدة node:test المدمجة في Node تكفي:

import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
  JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);

for (const f of readdirSync('fixtures/order')) {
  const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
  if (f.startsWith('valid-')) {
    test(`${f} يجب أن تجتاز`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} يجب أن تُرفض`, () => { assert.ok(!validate(data)); });
  }
}

لقطة الحالة: سرّد validate.errors في ملف وقارن. غيّر أحدهم قيدًا سهوًا — تتورد لقطة الحالة فورًا. في Jest/Vitest سطر واحد: expect(errors).toMatchSnapshot()؛ وفي CI مرّر --ci دائمًا: اللقطة الجديدة عندئذٍ فشل لا تُنشأ بصمت، ولا يُنال الانحراف أي تساهل.

3. اجمع كل الأخطاء في جولة واحدة

إعداد كثيرًا ما يُنسى عند اختبار السلبية: المدقق يتوقف افتراضيًا عند أول خطأ. البيانات مكسورة في ثلاثة مواضع، ترى واحدًا، تصححه، تشغّل مجددًا — وهكذا دواليك. مع allErrors: true (Ajv) أو iter_errors (Python) يظهر كل شيء دفعة واحدة:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
    print(list(e.absolute_path), e.validator, e.message)

ويحفظ بايثون كذلك حقيقةً ضد الحدس: الخطأ الذي يبلغ عنه validate() ليس "الأول" بل اختيار best_match. في قياسنا، بوجود ثلاث مخالفات (نوع id خاطئ، وrole خارج القائمة، وحقل extra زائد)، لم يُبلَّغ سوى additionalProperties — وبقي خطأ نوع id مستترًا. في CI استخدم iter_errors دومًا لتحصل على الصورة الكاملة.

والفخ الأسهل نسيانًا بعد: format ليس تأكيدًا افتراضيًا. تصنّف مسودة 2020-12 الصيغة تعليقًا توضيحيًا: مكتبة jsonschema في بايثون لا تتحقق منها دون تمرير format_checker — في اختبارنا مرّت "not-an-email" بحرية. ويطالب Ajv بتثبيت ajv-formats وتسجيلها. إن كانت الصيغة مهمة (بريد، تاريخ، uuid)، فيجب أن توجد عينة سلبية تثبت أن إعدادات مدققك تتحقق منها فعلًا.

4. جمّع مرة واحدة — لا تعامل المُجمِّع معاملة المستهلك

قياس محلي (Node v22.13.1، وAjv 8.17، والمخطط نفسه المتشعب ثلاثي المستويات، ونفس البيانات المخالفة):

الأسلوب الزمن لعشر آلاف تحقق
new Ajv() ثم compile في كل مرة 42233 ملي ثانية
نسخة على مستوى الوحدة + compile مرة واحدة 6.9 ملي ثانية

معامل 6000. مدقق داخل معالج طلبات ينشئ Ajv جديدًا عند كل استدعاء هو أكثر حوادث الأداء شيوعًا. وسجّل أيضًا الإجابة الخاطئة المنتشرة: "ajv.validate(schema, data) يعيد التجميع كل مرة". بالقياس: مئة ألف استدعاء استغرقت 8.2 ملي ثانية — قريبة من 11.2 ملي ثانية لدالة تُجمَّع مرة واحدة، لأن Ajv يحفظ المخططات في ذاكرة مؤقتة داخلية. المرض الحقيقي ليس فوات الكاش، بل النسخة التي لا يُعاد استخدامها. وفي بايثون الأمر ذاته: Draft202012Validator المعاد استخدامه أسرع بنحو 5000 مرة من استدعاء validate() كل مرة (2000 تكرار: 0.3 ملي ثانية مقابل 1566 ملي ثانية)، لأن الأخير يفحص المخطط نفسه عند كل استدعاء.

5. اِلحم البوابات في CI: أربعة أبواب

مرتبة من الأرخص إلى الأغلى: إن سقطت الأولى فلن تلزم الثواني:

البوابة شكل الأمر ماذا تصطاد الكلفة
فحص lint للمخطط node scripts/lint-schemas.mjs أخطاء الصياغة، الكلمات المتقادمة ملي ثوانٍ
التحقق من العينات حزمة الاختبارات في القسم 2 عدم تزامن العينة/المخطط، قيود موسومة خطأً ملي ثوانٍ
مقارنة التغييرات الكاسرة json-schema-diff-validator مقابل main required محذوف، enum مُضيَّق، pattern متشدد ثوانٍ
التحقق من ردود حقيقية اختبارات دخان على staging بتأكيد .toMatchSchema() انحراف نقاط النهاية الحقيقية ثوانٍ

أدنى إعداد لـ GitHub Actions:

- run: npm ci
- run: npm run lint:schemas        # Ajv2020 lint
- run: npm run test:schema         # ثلاثية العينات
- run: npx jest --ci --testPathPattern=schema   # حارس انحراف لقطات الحالة

بوابة التغييرات الكاسرة تستحق تأكيدًا مستقلًا: أدوات مثل json-schema-diff-validator تلوّن بالأحمر «حُذفت required» و«قيم enum ناقصة» منذ لحظة المراجعة — وهما بالضبط التغييران الذي يميل فرق API العادي إلى الحكم عليهما بـ «لا يكسر». حذف حقل من الرد غير مؤذٍ لمستهلِك متسامح، وقاتل لمستهلِك يستخدم switch شاملًا.

6. أكِّد على كائن الخطأ لا على قيمة منطقية فحسب

الاختبار السلبي الذي يكتفِ بتأكيد validate(data) === false لا يكفي. للمخطط نفسه، تأكد أنه يرفض للسبب الصحيح: قيد غريب قد يجتاز بالصدفة. لكائن خطأ Ajv عدم تماثل شهير: حين تفشل required يشير instancePath إلى الكائن الأب، واسم الحقل المفقود يختبئ في params.missingProperty. مقيسًا عند غياب id:

{ "instancePath": "", "schemaPath": "#/required",
  "keyword": "required", "params": { "missingProperty": "id" } }

يُكتب التأكيد بصيغة «توجد أخطاء keyword=required وmissingProperty=id»، لا بغامض errors.length > 0. التفكيك الكامل لدلالات حقول الخطأ موجود في أخطاء JSON Schema غير المقروءة: موضع الخطأ في Ajv ومعالجة الرسائل.

7. أخطاء شائعة — جدول سريع

الفخ العاقبة العلاج
اختبار نجاحات 200 فقط يفوت انحراف ردود الأخطاء مخططات وعينات لردود الأخطاء أيضًا
مخططات مولَّدة دون مراجعة required مستنتجة من مثال واحد ترفض بيانات حقيقية؛ يطفئ الفريق التحقق مراجعة بشرية لـ required/enum بعد التوليد
مخطط واحد لكل الإصدارات تُمحى فروق v1/v2 نسِّق المخططات مع نقاط النهاية
dump الحمولة كاملة في السجل عند الفشل تتسرب بيانات شخصية وأسرار إلى سجل CI اكتب مسارات JSON Pointer ومقتطفات مقصوصة فقط
enum متجمدة إعادة تسمية في المنتج (canceled→cancelled) تهدم التحقق تغييرات enum تمر بمراجعة توافق
إصدار المدقق غير مثبَّت سلوك مختلف بين الأجهزة ثبِّت إصدار المدقق في CI
استبدال اختبارات التفويض 403 قد يطابق مخطط الخطأ ويبقى منطق الأدوار خاطئًا المخطط يحرس البنية؛ التفويض يُختبر منفصلًا
نسيان دلالات format عناوين بريد عشوائية تمر بحرية ثبِّت ajv-formats / مرِّر format_checker وأضف سلبية

8. قائمة فحص التطبيق

  1. ملفات المخططات في إدارة الإصدارات، وnode scripts/lint-schemas.mjs ضمن lint؛
  2. لكل مخطط عينة إيجابية واحدة وواحدة سلبية لكل قيد على الأقل؛ اسم الملف هو التأكيد؛
  3. كائن الخطأ إلى لقطة حالة؛ وفي CI مرِّر --ci كي لا تنحرف اللقطة وحدها؛
  4. تُعاد استخدام نسخة المدقق على مستوى الوحدة — لا تنشئها داخل المعالج؛
  5. مقارنة التغييرات الكاسرة معلَّقة على PR؛ وتعديلات required/enum بمراجعة إلزامية؛
  6. أول PR يثبت عمدًا أن البوابة قد تسقط: احذف required، شاهد الأحمر، أعِده.

أما مسألة صرامة التحقق (متى تُغلق additionalProperties) فموضوع آخر — انظر additionalProperties مفتوحة: التحقق من المخطط لا ينفع. وهذا المقال عن جعل القواعد المكتوبة قابلة للتصديق دائمًا. وللأساسيات ابدأ من JSON Schema من الصفر إلى الهندسة.

FAQ

ما علاقة التحقق من المخططات باختبارات العقود على طريقة Pact؟

إنه تقسيم عمل. التحقق من المخطط يجيب عن «هل هذا المستند صحيح بنيويًا؟» — تأكيد أحادي الاتجاه. أما اختبارات العقود التي يقودها المستهلك (Pact) فتجيب عن «هل ما يزال المزود يفي باحتياجات المستهلك X تحديدًا؟» وتشترط تحققًا ثنائي الاتجاه. الفريق الصغير يبدأ ببوابات المخطط، ويتبنى Pact حين يتعدد عدد الخدمات.

كم اختبارًا سلبيًا يكفي؟

سلبي واحد لكل قيد هو الحد الأدنى: بما يقابل عدد قيود المخطط (required، type، minimum، enum، pattern، additionalProperties…)، مع أزواج قيم حدية مرفوضة/مقبولة. إن كانت السلبية أقل من القيود فثمّة قيد بلا حراسة.

أي تغييرات المخطط تُعدّ «كاسرة»؟

القيمة من زاوية المستهلك: حذف حقل من الرد، حذف enum أو تضييقه، تشديد pattern، إضافة حقل إلزامي للطلب — كلها تكسر؛ أما إضافة حقل اختياري للرد فآمن عادةً — بشرط أن يتسامح مُفك ترميز المستهلك مع المفاتيح الغريبة. أتمِت المقارنة بـ json-schema-diff-validator عند PR، والمناطق الرمادية قررها أنت.

الاختبار على واجهة حقيقية أم محاكاة؟

كلاهما، على طبقات: اختبارات العينات لا تلمس الشبكة، تجري بالميلي ثوانٍ، وتشتغل في كل PR؛ والتحقق الحقيقي يضرب staging، يكلف ثوانٍ، وينتمي لخط الدخان، ويُتخطى بـ skipif بلا بيانات اعتماد. الفريق الذي لا يشغّل في CI سوى الواجهات الحقيقية يُخنقه الشبكة المتذبذبة وحدود المعدل، ثم يطفئ الاختبارات في النهاية.

المراجع

مقالات ذات صلة

بدون additionalProperties، لا يتحقق تحقق المخطط من أي شيء

اجتياز التحقق لا يعني بيانات نظيفة. Pydantic يتجاهل الحقول غير المعروفة افتراضيًا، وZod يزيلها، وAjv يمررها — المعاملات المتخيلة والمفاتيح المكتوبة خطأً تعبر بصمت. يعرض هذا المقال ثلاث صور للقبول الصامت ويقدم طرقًا كاملة: إغلاق الكائنات، والتحكم في تحويل الأنواع، وشد كل مستوى تداخل.

Intermediateقراءة نحو 8 دقيقةحُدِّث في 2026-11-02

JSON Schema: من الأساسيات إلى الإنتاج

الكلمات المفتاحية كثيرة، لكن لا يُستخدم منها يوميًا إلا نحو دزينة. أتقن type/properties/required/$ref، وأحسِن رسائل الخطأ، فستغطي ثمانين بالمئة من الحالات الحقيقية.

متوسطقراءة نحو 14 دقيقةحُدِّث في 2026-10-30