JSON Toolkit

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

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

متوسطقراءة نحو 14 دقيقةنُشر في 2026-10-30حُدِّث في 2026-10-30
غلاف JSON Schema: من الأساسيات إلى الإنتاج

الخلاصة: ثبّت $schema على 2020-12 أولًا، ثم استخرج البنى المشتركة بـ $defs + $ref؛ واستخدم oneOf فقط عندما يجب أن يطابق فرع واحد بالضبط، وفضّل المميّزات أو anyOf في كل ما عدا ذلك.

المخطط ليس ملحقًا للتوثيق؛ بل أصل قابل للتنفيذ والاختبار والتوليد.

1. اضبط الإصدار أولًا

الإصدار قيمة $schema الحالة ملاحظات
draft-04 http://json-schema.org/draft-04/schema# قديم المشاريع القديمة وبعض أدوات OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# مستخدم على نطاق واسع الأساس الفعلي لـ OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema متاح أدخل $defs ومفردات التعليق
2020-12 https://json-schema.org/draft/2020-12/schema الموصي به حاليًا أدخل prefixItems، وغيّر دلالات items

استخدم 2020-12 للمشاريع الجديدة. ولاحظ أن items في 2020-12 تقيّد فقط "العناصر المتبقية" — فقد انتقلت تصريحات نمط الصفوف إلى prefixItems. وهذا أكثر فخ شيوعًا عند الترحيل من draft-07.

2. مخطط بسيط صالح للاستخدام

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

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

3. خريطة الكلمات المفتاحية

3.1 عامة

الكلمة المفتاحية الغرض ما يجب الحذر منه
type النوع؛ وقد يكون مصفوفة "type": ["string", "null"] يعني قابلية null
enum قيم معدودة يجب أن تكون القيم فريدة
const قيد قيمة واحدة جيد للقيم الثابتة مثل رقم الإصدار
$ref يشير إلى مخطط آخر يشير إلى المخطط بأكمله ولا يندمج مع الكلمات الشقيقة في الكائن المُحيل

3.2 الأعداد

multipleOf وmaximum وexclusiveMaximum وminimum وexclusiveMinimum. لاحظ أن exclusiveMaximum كانت قيمة منطقية في draft-04 (تُستخدم مع maximum)؛ ولم تصبح كلمة مفتاحية رقمية مستقلة إلا اعتبارًا من draft-07 — وهو ما يجعل كثيرًا من "المخططات المنسوخة من توثيق قديم" تفشل في الوضع الصارم.

3.3 السلاسل

minLength وmaxLength وpattern (تعبير نمطي ECMA-262) وformat (تعليق).

يعد minLength نقاط ترميز Unicode، لا وحدات UTF-16 ولا بايتات. والتحقق بـstring.length في JavaScript سيختلف معه.

3.4 المصفوفات

الكلمة المفتاحية الغرض
prefixItems قيد نمط الصفوف في 2020-12، يطابق العناصر واحدًا واحدًا بالفهرس
items قيد موحّد على العناصر المتبقية (2020-12)
contains + minContains/maxContains أدنى/أقصى عدد من العناصر المطابقة الواجب وجوده
uniqueItems يجب أن تختلف العناصر كلها (مقارنة عميقة؛ تجنبها في المصفوفات الكبيرة)
minItems / maxItems قيود الطول

3.5 الكائنات

properties وrequired وpatternProperties وadditionalProperties وpropertyNames، وminProperties وmaxProperties وdependentRequired وdependentSchemas.

ثلاثة مفاهيم خاطئة عالية التكرار:

  1. required تشترط وجود المفتاح فقط: فـ{"name": ""} صالح. ولرفض سلسلة فارغة يجب إضافة minLength: 1؛
  2. additionalProperties: false تضخّم مشكلات التوافق: فحقل إضافي واحد من عميل يفشل الحمولة بأكملها، فيفرض تطور API على كل مستدعٍ الترقية بالتزامن؛
  3. properties لا تقيّد المفاتيح غير المعلنة — فتُسمح بها افتراضيًا.

3.6 التركيب المنطقي

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
الكلمة المفتاحية الدلالة الفشل النمطي
allOf يجب أن تتحقق كلها عدة فروع تفرض قيودًا متعارضة على الحقل نفسه
anyOf يجب أن يتحقق واحد على الأقل يصعب تتبع رسالة الخطأ إلى فرع محدد
oneOf يجب أن يتحقق واحد بالضبط البيانات تطابق فرعين فتفشل، رغم أنها "تبدو صحيحة بوضوح"
not يجب ألا تتحقق نادر الاستخدام، وأقل رسائل الخطأ ودية

oneOf هي الكلمة المفتاحية الأكثر قدرة على إنتاج "بيانات صالحة تفشل في التحقق". فإذا تداخلت قيود الفروع (مثلًا يسمح كلاهما بحقول إضافية)، فإن سجلًا واحدًا يطابق عدة فروع ويفشل oneOf مباشرةً. وفي هذه الحالة انتقل إلى anyOf، أو استخدم حقل مميّزًا فيه const داخل الفروع.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

نمط المميّز أكثر وضوحًا في القراءة من oneOf، وينتج أخطاءً أفضل.

4. التقسيم وإعادة الاستخدام

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs هي الصيغة المُدخلة في 2019-09؛ ويستخدم draft-07 التسمية definitions؛
  • تستخدم الإحالات عبر الملفات $id نسبية، لكن لا تعتمد في الإنتاج على جلب مخططات بعيدة في وقت التشغيل — بل اجمعها ملفات ثابتة؛
  • لا يمكنك إضافة قيود بجوار الإحالة (فسلوك $ref مع الكلمات الشقيقة يختلف حسب الإصدار). وعندما تحتاج إلى تركيب، استخدم allOf.

5. التحقق في وقت التشغيل بـ Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

نقاط أساسية:

  • استدعِ compile مرة واحدة فقط، في المستوى الأعلى للوحدة؛ فإعادة الترجمة في كل طلب تحوّل تحققًا يستغرق 10 ملّيثانية إلى مئات الملّيثانيات؛
  • يُظهر strict: true الكلمات المفتاحية المكتوبة خطأً مبكرًا (مثل minlength بدلًا من minLength)، لذا يستحق التفعيل؛
  • لا تجعل format: "email" وformat: "date-time" فعّالة إلا ajv-formats؛
  • يشترط 2020-12 نقطة الدخول ajv/dist/2020 — لا تستخدم التصدير الافتراضي.

6. كيف تصمم رسائل الخطأ

ما يمنحك إياه المدقق هو "أي كلمة مفتاحية فشلت"؛ وما يريده المستخدم هو "أي مربع إدخال خطأ". ويستحق الأمر طبقة ربط:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'الاسم',
  '/age': 'العمر',
  '/tags': 'الوسوم',
};

/**
 * يحوّل أخطاء Ajv إلى رسائل جاهزة للعرض.
 * @param errors قائمة أخطاء Ajv
 * @returns مصفوفة رسائل للعرض
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} يفتقد الحقل المطلوب ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} قصير جدًا`;
    }
    return `${label} فشل في التحقق (${errorItem.keyword})`;
  });
}

وعلى جانب API، احفظ بنية قابلة للقراءة آليًا حتى تقرر الواجهة كيفية عرضها:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "فشل التحقق من بيانات الطلب",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "يجب أن يكون سلسلة نقدية بمنزلتين عشريتين" }
  ]
}

7. الأداء والمزالق

  • تنمو تكلفة التحقق مع تعقيد المخطط وحجم البيانات؛ وتسبب فروع oneOf المتعددة انفجارًا توافقيًا، وهو ما يصبح ملحوظًا جدًا مع آلاف السجلات؛
  • ينفّذ uniqueItems مقارنة عميقة O(n²) على المصفوفة؛ وللمصفوفات الكبيرة، أزل التكرار في طبقة العمل بدلًا من ذلك؛
  • تجنّب أنماط التتبع العكسي الكارثي في pattern (مثل (a+)+$)؛ فهذا هو المدخل الكلاسيكي لـ ReDoS؛
  • عندما تُدمج additionalProperties: false مع allOf، فالحقول المعلنة في مخطط فرعي تُعدّ "إضافية" من قبل الأب، فيلزمك دمجها في مخطط واحد أو استخدام unevaluatedProperties.

8. قائمة التحقق

  • [ ] ثبّت 2020-12 وأعلن $schema في أعلى الملف؛
  • [ ] استخرج البنى المشتركة بـ $defs + $ref بدلًا من النسخ واللصق؛
  • [ ] فضّل المميّزات على oneOf؛
  • [ ] أعد استخدام نتيجة compile في المدقق وفعّل الوضع الصارم؛
  • [ ] وثّق بوضوح هل تُفرض format؛
  • [ ] توفر استجابات الأخطاء المسار والكلمة المفتاحية ورسالة موجهة للمستخدم؛
  • [ ] ضع المخطط في CI: شغّل حالات JSON Schema Test Suite كاختبارات انحدار.

الأسئلة الشائعة

كيف أختار إصدار JSON Schema؟

استخدم 2020-12 للمشاريع الجديدة وأعلن $schema في أعلى الملف. وإذا كان المخطط سيدخل OpenAPI 3.1، فيجب أن يكون 2020-12 أيضًا، لأن 3.1 يضمّن ذلك الإصدار مباشرةً؛ واحفظ draft-07 للمشاريع المرتبطة بسلاسل أدوات أقدم فقط. ويختلف سلوك الاثنين في items وفي الكلمات الشقيقة لـ $ref، فلا تخلطهما في مجموعة مخططات واحدة.

لماذا تضخّم additionalProperties: false مشكلات التوافق؟

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

هل أستخدم oneOf أم anyOf؟

استخدم oneOf فقط عندما يجب أن تطابق البيانات فرعًا واحدًا بالضبط. فإذا كان بالإمكان تحقق قيود الفروع معًا، فسجل واحد يطابق عدة فروع ويفشل oneOf مباشرةً؛ فانتقل إلى anyOf، أو أضف مميزًا بـ const داخل كل فرع.

المراجع

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

مزامنة ثنائية الاتجاه بين المخطط وأنواع TypeScript

الأنواع والمخططات يغطي كل منهما مقطعًا واحدًا: أحدهما وقت الترجمة والآخر وقت التشغيل. السؤال ليس أيّهما تختار، بل كيف تُبقي الاثنين متزامنين تلقائيًا.

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

النقاط الأساسية لتصميم عقود بيانات JSON API

ستة أمور يصعب جداً تغييرها بعد النشر: أغلفة الاستجابة، صيغ الأخطاء، شكل الترقيم، تمثيل المعرفات والوقت، دلالات null مقابل الغياب، وتطور الإصدار.

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

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

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

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