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

الخلاصة: اقفل غلاف الاستجابة، وصيغة الأخطاء، وتمثيل المعرف والوقت، ودلالات null مقابل الغياب في العقد قبل مناقشة تفاصيل الترقيم والتسمية؛ وغلّف المستوى الأعلى دائماً في غلاف data حتى يمكن إضافة الترقيم ومعرفات التتبع لاحقاً دون كسر المستدعين الحاليين.
لا توجد إجابات قياسية في تصميم API، لكن توجد بعض القرارات "الصعب استرجاعها بعد الاختيار" التي يُستحسن التفكير فيها قبل كتابة أول سطر كود.
1. تغليف الاستجابة أم لا
ثلاثة أنماط:
// النمط A: استجابة عارية
{ "id": 1, "name": "张三" }
// النمط B: غلاف data
{ "data": { "id": 1, "name": "张三" } }
// النمط C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
| النمط | الإيجابيات | السلبيات |
|---|---|---|
| استجابة عارية | الأبسط، طبقة أقل للواجهة | لا يمكن إرفاق بيانات وصفية؛ المصفوفة في المستوى الأعلى تحمل مخاطر اختطاف JSON والتطور |
| غلاف data | يسهل إضافة meta / errors / links |
مستوى تداخل إضافي، ويجب أن تكتب الواجهة res.data.data |
| data + metadata | أفضل قابلية للملاحظة | يتطلب اتفاقاً صارماً على الحقول التي تنتمي إلى meta |
التوصية: استخدم غلاف data بشكل موحّد للقوائم والتفاصيل معاً، ولا تجعل المستوى الأعلى مصفوفة أبداً. بذلك لا تكسر إضافة الترقيم أو معرفات التتبع العقد لاحقاً.
2. صيغة الأخطاء: انسخ RFC 9457
اختراع بنية أخطاء خاصة بك مصدر ضخم لتكلفة التعاون. يحدد RFC 9457 (الذي يحل محل RFC 7807) نوع application/problem+json:
{
"type": "https://bbdrug.com/problems/insufficient-balance",
"title": "余额不足",
"status": 422,
"detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
"instance": "/accounts/A1001/payments/p-889",
"balance": "12.00",
"required": "30.00"
}
| الحقل | المعنى | مطلوب |
|---|---|---|
type |
URI ثابت لنوع المشكلة | مستحسن |
title |
ملخص قصير قابل للقراءة البشرية | مستحسن |
status |
رمز حالة HTTP | مستحسن |
detail |
شرح محدد لما حدث هذه المرة | اختياري |
instance |
نسخة المورد التي واجهت المشكلة | اختياري |
| حقول الامتداد | حقول أعمال اعتباطية | اختياري |
حقول الامتداد (مثل balance / required أعلاه) تسمح بها المواصفات، ويمكن للواجهة استخدامها لرسائل دقيقة بدلاً من تحليل نص detail.
3. الترقيم: المؤشر أفضل من الإزاحة
// ترقيم بالإزاحة
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }
// ترقيم بالمؤشر
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
| البُعد | ترقيم بالإزاحة | ترقيم بالمؤشر |
|---|---|---|
| صعوبة التنفيذ | منخفضة (LIMIT/OFFSET) |
متوسطة (يتطلب مفتاح فرز مستقر) |
| الأداء في الصفحات العميقة | ضعيف (يزداد الإزاحة سوءاً) | جيد |
| عند تغيّر البيانات | تكرارات أو عناصر مفقودة | مستقر |
| الانتقال إلى صفحة | ممكن | غير ممكن |
| يحتاج إلى عدد إجمالي | يتطلب عدّاً إضافياً | لا يحتاج |
التوصية: استخدم ترقيم الإزاحة لجداول الإدارة (التي تحتاج قفزات صفحات ومجاميع)، وترقيم المؤشر لخلاصات المستخدمين اللانهائية.
4. المعرفات والوقت
4.1 استخدم النصوص دائماً للمعرفات
جاء المنطق في مقالة الأنواع: على جانب JavaScript، تفقد القيم فوق 2^53 دقتها بصمت.
وحتى لو كان المعرف الحالي عدداً صحيحاً صغيراً يتزايد تلقائياً، فإن استخدام النصوص يترك مجالاً للتحول لاحقاً إلى معرفات snowflake.
{ "id": "1000000000000000001" }
4.2 استخدم نصوص RFC 3339 للوقت
{
"createdAt": "2026-10-30T10:41:45+08:00",
"updatedAt": "2026-10-30T02:41:45Z"
}
- لا تستخدم طوابع زمنية رقمية بالميلي ثانية: فهي صعبة القراءة، والوحدة (ثوانٍ/ميلي ثانية) تُخطأ كثيراً؛
- حدد المنطقة الزمنية صراحةً: إما بـ
Zأو بإزاحة؛ ولا تستخدم أبداً "وقت محلي بلا منطقة زمنية"؛ - سمّ الحقول بشكل موحّد
xxxAtحتى يتضح من النظرة الأولى أنه نقطة زمنية.
5. null والغياب
يجب أن تختار واحدة وتكتبها في الوثائق:
| النهج | التعبير عن "لا قيمة" | التعبير عن "لا تعدّل" |
|---|---|---|
| النهج A | قيمة الحقل null |
احذف الحقل من الطلب |
| النهج B | الحقل غير موجود | احذف الحقل من الطلب |
مشكلة النهج A أن "محذوف" و"مرسل كـnull" يعنيان شيئين مختلفين في PATCH، وهو أمر يسهل إساءة تفسيره؛
ومشكلة النهج B أن JSON.stringify في الواجهة يُسقط الحقول التي قيمتها undefined، وهو ما يصادف تطابق الدلالات.
التوصية العملية:
- استخدم النهج A لأجسام الاستجابة (
nullيعني بوضوح لا قيمة، والواجهة لا تحتاج إلى تمييزundefined)؛ - استخدم النهج B لأجسام الطلب (خصوصاً
PATCH)، مع Merge Patch أو JSON Patch لجعل الدلالات صريحة.
6. تسمية الحقول والبنية
| القرار | الخيارات الشائعة | تحفظ |
|---|---|---|
| أسلوب التسمية | camelCase (نظام JS) / snake_case (Python، قواعد البيانات) |
موحّد في المشروع، لا تخلط |
| قيم enum | أحرف صغيرة بشرطات سفلية، "pending_review" |
لا تستخدم أرقاماً؛ فهي صعبة القراءة |
| الحقول المنطقية | isActive / enabled |
لا تستخدم status: 0/1 |
| حقول المصفوفات | أسماء جمع، items |
أعِد [] للمصفوفات الفارغة، لا null أبداً |
| عمق التداخل | لا يزيد على 3 مستويات | أعمق من ذلك، فقسّمه أو اعرض معامل expand |
إعادة [] بدلاً من null للمصفوفات الفارغة مهمة جداً: فلن يتعطل data.items.map(...) في الواجهة بسبب null.
7. استراتيجية التطور
| التغيير | يكسر التوافق | كيفية التعامل |
|---|---|---|
| إضافة حقل اختياري | لا | انشره مباشرة |
| إضافة حقل مطلوب | نعم | أضف قيمة افتراضية، أو أطلقه بإصدار |
| إزالة حقل | نعم | علّمه كمهجور أولاً، راقب المستدعين، ثم أزله |
| تغيير نوع حقل | نعم | أضف اسم حقل جديداً واكتب dual-write أثناء الانتقال |
| تغيير مجموعة قيم enum | يعتمد | يجب أن يكون لدى العميل بديل للقيم غير المعروفة |
| تغيير دلالات الحقل | نعم | عاملها كحقل جديد |
ثلاث قواعد انضباط:
- يجب أن يكون العملاء متسامحين مع الحقول غير المعروفة (لا تحصر نفسك بـ
additionalProperties: false)؛ - يجب أن يكون لدى العملاء فرع بديل لقيم enum غير المعروفة؛
- احتفظ بالحقول المهجورة لمدة إصدار رئيسي واحد على الأقل، وعلّمها بـ
Deprecationفي ترويسات الاستجابة أو الوثائق.
8. مثال كامل
{
"data": [
{
"id": "1001",
"name": "JSON 权威指南",
"price": "89.50",
"tags": ["json", "spec"],
"publishedAt": "2026-09-01T00:00:00+08:00",
"author": null
}
],
"page": { "nextCursor": null, "hasMore": false },
"meta": { "requestId": "req-7f3a", "tookMs": 12 }
}
تعكس هذه الاستجابة كل الاتفاقيات أعلاه: غلاف data، معرفات نصية، مبالغ كنصوص، طوابع زمنية بمنطقة زمنية،
null بمعنى مفقود، مصفوفة فارغة، ترقيم بالمؤشر، وmeta لمعلومات القابلية للملاحظة.
9. قائمة الفحص
- [ ] المستوى الأعلى ليس مصفوفة؛ استخدم غلاف
dataبشكل موحّد؛ - [ ] استجابات الأخطاء تتبع RFC 9457 وتشمل
typeثابتاً؛ - [ ] شكل الترقيم يطابق سيناريو الأعمال؛
- [ ] المعرفات نصوص، والوقت يستخدم RFC 3339؛
- [ ] دلالات
nullوالغياب مكتوبة في الوثائق؛ - [ ] أسلوب التسمية موحّد في المشروع، وenums تستخدم نصوصاً صغيرة؛
- [ ] العميل لديه بدائل للحقول غير المعروفة وقيم enum غير المعروفة؛
- [ ] التغييرات الكاسرة تمر عبر إصدارات أو انتقال dual-write.
FAQ
كيف أصمم JSON API قابلة للتطور؟
غلّف القوائم والتفاصيل بشكل موحّد في غلاف data ولا تجعل المستوى الأعلى مصفوفة أبداً، حتى يمكن إضافة الترقيم ومعرفات التتبع دون كسر العقد. استخدم معرفات نصية وطوابع زمنية RFC 3339، واكتب دلالات null مقابل الغياب في الوثائق. أبقِ الحقول الجديدة اختيارية ومستقلة دلالياً، وامنح العملاء بدائل للحقول غير المعروفة وقيم enum.
لماذا لا أبتكر صيغة أخطاء مخصصة؟
اختراع بنية أخطاء خاصة بك مصدر ضخم لتكلفة التعاون. انسخ application/problem+json من RFC 9457، وحدد المشكلات بـtype ثابت، واسمح بحقول امتداد مثل balance وrequired، حتى تبني الواجهة رسائل دقيقة دون تحليل نص detail.
هل يجب أن يستخدم كل endpoint ترقيم المؤشر؟
لا. جداول الإدارة تحتاج قفزات صفحات ومجاميع، لذا يناسبها ترقيم الإزاحة أكثر؛ وخلاصات المستخدمين اللانهائية يجب أن تستخدم ترقيم المؤشر، الذي يبقى مستقراً في الصفحات العميقة ويتجنب التكرارات أو الفجوات عند تغيّر البيانات. اختر بناءً على ما إذا كان السيناريو يحتاج قفزات صفحات ومجاميع.