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

الخلاصة: ثبّت $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.
ثلاثة مفاهيم خاطئة عالية التكرار:
requiredتشترط وجود المفتاح فقط: فـ{"name": ""}صالح. ولرفض سلسلة فارغة يجب إضافةminLength: 1؛additionalProperties: falseتضخّم مشكلات التوافق: فحقل إضافي واحد من عميل يفشل الحمولة بأكملها، فيفرض تطور API على كل مستدعٍ الترقية بالتزامن؛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 داخل كل فرع.