التحقق من JSON في CI: خط أنابيب بـ jsonlint وajv وjq
الخلاصة: يثبت التحقق من الصياغة أن الملف يُحلل، ويثبت التحقق من المخطط أن الحقول تتبع العقد. في CI، المهم هو رموز خروج لا لبس فيها ومواقع أخطاء ومسارات إصلاح.

الخلاصة: في CI، دع jq empty يتولى الصياغة، وajv-cli يتولى البنية (check-jsonschema للفرق متعددة الصيغ)، وPrettier يتولى النمط؛ اربطها طبقة بطبقة وتوقف عند أول فشل.
اجتياز المحلل في محررك المحلي لا يعني أن الملف ينتمي إلى CI؛ واجتياز CI لا يعني أن الحقول تطابق العقد أيضًا.
1. افصل التحقق من الصياغة عن التحقق من المخطط
يُستخدم مصطلح "التحقق من JSON" على نطاق واسع أكثر من اللازم، ولهذا تختار الفرق غالبًا أداة واحدة لحل مشكلتين مختلفتين:
| الطبقة | السؤال المطروح | الأدوات النموذجية | ماذا يعني الفشل |
|---|---|---|---|
| الصياغة | هل هذا الملف JSON صالح؟ | jq empty، jsonlint، python -m json.tool |
الأقواس أو الفواصل أو علامات الاقتباس أو الترميز مكسورة |
| المخطط | هل الحقول والأنواع والخصائص المطلوبة تطابق العقد؟ | ajv-cli، check-jsonschema |
الملف يُحلل، لكن البنية التجارية خاطئة |
| النمط | هل الإزاحة البادئة وترتيب المفاتيح متسقان؟ | Prettier، jq -S |
التنسيق غير متسق والفروق مثيرة للضجيج |
لا يمكن لهذه الطبقات أن تحل محل بعضها. الملف الذي يجتاز الصياغة قد يفشل في المخطط: {"price": "12.5"} صالح كـ JSON، لكن إن كان المخطط يشترط أن يكون price عددًا، فهو غير مقبول. وبالعكس، مهما كان المخطط كاملًا، فلن يُترجم ملف ينقصه قوس إغلاق مقابله أبدًا.
لذا يجب أن يشغّل خط الأنابيب الصياغة أولًا، ثم البنية، ثم النمط أخيرًا. توقف فور فشل أي طبقة، وإلا فسيتنكر خطأ التحليل في هيئة خطأ حقل.
2. طبقة الصياغة: jq empty هو الخيار الأقل تبعية
إن كان jq موجودًا أصلًا في صورة CI، فالتحقق من الصياغة لا يكاد يكلف شيئًا:
jq empty data.json
لا ينتج jq empty أي خرج؛ إنه يحلل الملف فقط. يخرج بالرمز 0 عند النجاح ورمز غير صفري مع معلومات السطر/العمود عند الفشل. هذا يجعله أخف من سحب شجرة تبعيات Node كاملة، وبوابة أولى جيدة.
عند التحقق الدفعي من ملفات JSON في مستودع، استبعد node_modules ومخرجات البناء وذاكرات الأطراف الثالثة:
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
يعالج -print0 وxargs -0 المسافات في أسماء الملفات، بينما يضمن -n1 ملفًا واحدًا لكل استدعاء فيحدد رسالة الخطأ الجاني. لا تمرر مئات الملفات إلى عملية واحدة؛ فستتغطى رسائل الخطأ بعضها على بعض.
jsonlint خيار شائع آخر، يُستدعى عادة بـ npx jsonlint -q data.json، حيث يطبع -q الأخطاء فقط. يبلّغ عن أرقام الأسطر والأعمدة، لكن صيانته أقل نشاطًا من jq، وتختلف إصداراته في تسامحها مع BOM والتعليقات. اعتبره فاحص صياغة أوضح قراءةً، لا مدقق مخطط.
3. طبقة المخطط: أساسيات ajv-cli validate
Ajv هو مدقق المخططات الأكثر استخدامًا في منظومة JavaScript، ويكشفه ajv-cli كأداة سطر أوامر:
ajv validate -s schema.json -d data.json
يشير -s إلى ملف المخطط و-d إلى البيانات المدققة. عند الفشل تخرج العملية برمز غير صفري، فيفشل CI فورًا. تضيف المشاريع الحقيقية عادة علمين:
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
يساعد --strict=false مع المخططات الأقدم: إذ يعامل الوضع الصارم الكلمات المفتاحية غير المعروفة وإعلانات required المشبوهة كأخطاء، وهي ما تفشل فيه كثير من المخططات القديمة. ينتج --errors=text خرجًا مقروءًا للبشر، بينما تناسب بنية JSON الافتراضية الآلات أكثر. اختر بحسب من يستهلك النتيجة.
يجب أن يتطابق إصدار مسودة المخطط أيضًا. تدعم إصدارات Ajv الرئيسية مسودات مختلفة افتراضيًا، ويستطيع ajv-cli اختيار واحدة بـ --spec=draft7 أو --spec=draft2019 أو --spec=draft2020. إن أعلن مخطط ما عن $schema: "https://json-schema.org/draft/2020-12/schema" لكن الأداة تُترجمه كـ draft-07، فستُتجاهل بعض الكلمات المفتاحية بصمت. لمزيد عن تأليف المخططات وفروق الإصدارات، راجع البدء مع JSON Schema.
التحقق من الصيغة متطلب متكرر آخر. الكلمة المفتاحية format توضيحية فقط افتراضيًا؛ ولكي تفرض email وdate-time وuri شيئًا فعليًا، حمّل ajv-formats:
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
من السهل تفويت هذه الخطوة، والنتيجة أن حقل بريد يقبل not-an-email. قبل التسليم، أطعم خط الأنابيب سجلًا غير صالح عمدًا؛ فذلك يثبت أن القاعدة فعّالة أفضل من قراءة التوثيق عشر مرات.
check-jsonschema: صيغ متعددة وأخطاء ألطف
إن كانت الفرقة تتعامل مع YAML أو TOML أو إعداد GitHub Actions إضافة إلى JSON، فـ check-jsonschema أكثر ملاءمة:
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
مزاياه العملية:
- دعم YAML أصلي، يناسب الملفات "الشبيهة بـ JSON والمكتوبة بـ YAML"، مثل manifests الخاصة بـ Kubernetes وإعداد CI.
- تتضمن الأخطاء مسار الملف ورقم السطر ومسار الحقل، ما يقلل كلفة التحديد.
- يوفر خطاف pre-commit، فتكلفة الدمج منخفضة.
- يمكنه سحب مخططات شائعة من SchemaStore، لكنه يقبل أيضًا ملفات محلية، فيتجنب الاعتماد على الشبكة.
في الفرق متعددة اللغات، غالبًا ما يكون Python موجودًا أصلًا، فتركيب check-jsonschema وترقيته أخف من صيانة سلسلة أدوات Node. دع jq يتولى الصياغة وهذه الأداة تتولى البنية؛ فالتوليف منخفض الكلفة الصيانة جدًا.
4. رموز الخروج هي الإشارة الموثوقة الوحيدة في CI
مهما كان الخرج مصقولًا، لا يقرأ CI سوى رموز الخروج. ثلاث مصائد شائعة:
| العَرَض | السبب | الإصلاح |
|---|---|---|
| توجد أخطاء لكن CI أخضر | الأمر ينتهي بـ ` | |
| لا توجد أخطاء لكن CI أحمر | يعامل diff "العثور على اختلافات" كفشل |
ميّز "الاختلاف" عن "خطأ التنفيذ" |
| خرج الأخطاء مقتطع | خرج متعدد الملفات المتوازي يغطي بعضه بعضًا | شغّل تسلسليًا أو اطبع لكل ملف |
تذكر دلالات رمز الخروج في jq: التحليل الطبيعي يعيد 0؛ ومع -e، تعيد نتيجة التعبير false أو null الرمز 1؛ أخطاء الاستخدام والملفات المفقودة تعيد 2؛ وأخطاء ترجمة السكربت تعيد 3. تستخدم أدوات المخططات عمومًا 0 للنجاح و1 لفشل التحقق و2 لأخطاء الأداة أو الوسائط. عند كتابة غلاف، لا تحوّل كل رمز غير صفري إلى "البيانات خاطئة"، وإلا فسيُشخَّص فشل بيئي كخطأ محتوى.
5. pre-commit: التقط المشكلات قبل الإيداع
قيمة pre-commit ليست "فحصًا إضافيًا"، بل إعطاء المطورين ملاحظات في دقيقة بدلًا من انتظار تشغيل CI عشر دقائق. يبدو إعداد بسيط هكذا:
repos:
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-jsonschema
files: ^configs/.*\.json$
args:
- --schemafile
- schemas/config.json
- repo: local
hooks:
- id: jq-syntax
name: jq syntax check
entry: jq empty
language: system
files: ^configs/.*\.json$
تفصيلان مهمان:
- يجب أن يطابق
filesفقط المجلدات التي تحتاج التحقق. لا تشغّل الخطافات علىnode_modulesأو الملفات المولّدة. - يفترض الخطاف المحلي أن
jqمثبت على جهاز المطور. وثّق هذا المتطلب، أو انتقل إلى خطاف في حاوية.
إن كان المشروع يستخدم Husky وlint-staged أصلًا، فاجمعهما مباشرة:
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
تذكر أن lint-staged يمرر قائمة ملفات مهيأة، لذا يجب أن يقبل الأمر مسارات متعددة. يقبل ذلك كلٌّ من jq empty وprettier --check. لا تدع خطافًا يمسح المستودع كاملًا؛ فستصبح عمليات الإيداع أبطأ ملحوظًا.
6. GitHub Actions: خط أنابيب قابل للنسخ
ينفذ سير العمل التالي التحقق من الصياغة والتحقق من المخطط ويحتفظ بالتقرير عند الفشل:
name: json-validate
on:
push:
paths:
- 'schemas/**'
- 'fixtures/**'
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check
run: |
find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
- name: Schema check
run: |
npx --yes ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
يمنع مرشّح paths التغييرات التوثيقية فقط من إطلاق التحقق. يناسب npx --yes المستودعات دون تبعيات مثبتة؛ أما مشاريع الإنتاج فتنبغي إضافة ajv-cli إلى devDependencies وتثبيت الإصدار. فدون قفل، قد يحوّل إصدارا جديد في المنبع يغيّر المسودة الافتراضية أو الوضع الصارم خط الأنابيب إلى أحمر بين ليلة وضحاها.
7. موقع الخطأ: من أرقام الأسطر إلى مسارات الحقول
نصف قيمة المدقق كشفية، والنصف الآخر تحديد موقع. تعلم قراءة ثلاثة أنواع من الخرج:
- أخطاء الصياغة: يبلّغ
jqعن السطر والعمود، غالبًا مع مقتطف قريب. أصلح الأقواس والفواصل أولًا؛ لا تشتبه في الترميز فورًا. - أخطاء المخطط: يشير
instancePathفي Ajv إلى موضع البيانات (مثل/items/0/price)، ويشيرschemaPathإلى القاعدة (مثل#/properties/items/items/properties/price/type)، ويسمّيkeywordالقيد المنتهك. - أخطاء الصيغة: عندما يفشل فحص
format، تقول الرسالة غالبًا فقط "must match format "email"". ادمجها معinstancePathلتحديد الحقل.
الصق instancePath في سجل التحقيق؛ فهو أنفع بكثير من جدار من JSON. وفي المصفوفات، يخبرك الفهرس الرقمي أيضًا أي سجل فشل. إن كان المفتاح التجاري أكثر بداهة، فأضف سطر تعيين بعده، مثل items[0].id = ORD-042.
تُغطي أخطاء المخطط الشائعة، مثل الإفراط في additionalProperties: false أو كتابة فروع oneOf متداخلة فعليًا، في مصائد توليد JSON Schema الشائعة. قاعدة تحقق خاطئة أخطر من عدم التحقق، لأنها تخلق شعورًا زائفًا بالأمان.
8. جدول اختيار الأداة
| الحاجة | الأداة الموصى بها | لماذا | انتبه إلى |
|---|---|---|---|
| الصياغة فقط | jq empty |
صفر تبعيات، سريع، رموز خروج واضحة | المسافات في أسماء الملفات أثناء التشغيل الدفعي |
| أخطاء سطر/عمود ألطف | jsonlint |
خرج مقروء | نشاط الصيانة محدود |
| التحقق من JSON Schema | ajv-cli |
منظومة ناضجة، أخطاء منظمة | طابق إصدارات المسودة وحمّل دعم format |
| مستودع يخلط JSON وYAML | check-jsonschema |
متعدد الصيغ، صديق لـ pre-commit | مصدر المخطط والوصول إلى الشبكة |
| ملاحظات سريعة قبل الإيداع | pre-commit / lint-staged | يعالج الملفات المهيأة فقط | يجب أن تقبل الخطافات مسارات متعددة |
| تنسيق متسق | Prettier | معتمد على نطاق واسع | استخدم --check عندما لا تريد إعادة كتابة الملفات |
توصية أخيرة: تحقق من أن خط الأنابيب يفشل فعلًا بإطعامه سجلًا غير صالح عمدًا أولًا. الخطر الأكبر ليس إعدادًا معقدًا؛ بل إعداد لا يسري أبدًا. معاملة "يمكن أن يفشل" كمعيار قبول أكثر موثوقية من معاملة "يجتاز" كمعيار. لسلسلة أدوات سطر الأوامر الأشمل، تابع المحررات وسطر الأوامر والأتمتة.
الأسئلة الشائعة
هل أستخدم jq empty أم jsonlint؟
فضّل jq empty في CI: صفر تبعيات، وبدء سريع، ودلالات رمز خروج واضحة. استخدم jsonlint عندما تحتاج رسائل سطر/عمود ألطف أو عندما تحقق محليًا. كلاهما يتحقق من الصياغة فقط؛ ولا يفحص أي منهما بنية الحقول.
لماذا يبلّغ ajv عن كلمة مفتاحية غير معروفة؟
أكد أولًا أن مسودة $schema في المخطط تطابق الإصدار الذي تستخدمه الأداة. كثير من أخطاء "الكلمة المفتاحية غير المعروفة" هي كلمات مفتاحية من مسودة جديدة يواجهها مترجم أقدم. إن تطابقت الإصدارات، فابحث عن خطأ كتابي. يمكن أن يُبقي --strict=false المخططات القديمة عاملة، لكن لا تعتمد عليه طويل الأمد لإخفاء مشكلات حقيقية.
يجتاز التحقق من المخطط، لكن API ما زال يفشل. لماذا؟
يتحقق المخطط من البنية لا من الدلالات التجارية. مثلًا، قد يكون status نصًا بقائمة enum قانونية، بينما تمنع آلة الحالة الانتقال مباشرة من paid إلى refunded. قواعد كهذه تنتمي إلى الشيفرة أو قيود قاعدة البيانات، لا إلى JSON Schema.
كيف أحدد الحقل الفاشل بسرعة؟
اقرأ instancePath أو رقم السطر من خرج الأداة. instancePath في Ajv هو JSON Pointer يشير مباشرة إلى البيانات؛ ويبلّغ check-jsonschema عن الملف والسطر. سجّل المسار وعيّنه على المفتاح التجاري؛ وتحديد المشكلة يستغرق عادة دقائق.
هل يجب أن يشغّل pre-commit تحققًا كاملًا؟
لا. يجب أن يعالج pre-commit الملفات المهيأة فقط لإبقاء الإيداعات سريعة. المسوح الكاملة تنتمي إلى CI أو الوظائف المجدولة. التقسيم بسيط: التشغيل المحلي يفضّل السرعة، وCI يفضّل الاكتمال.