البداية · فصل ١

المشكلة — الـ JSON ملوش عقد

no types at runtime · trust boundary · fail fast · contract · garbage in
🎯 الهدف: تفهم ليه البيانات اللي جاية من بره خطر، وليه TypeScript لوحده مش كفاية، وإمتى تحتاج JSON Schema.

الـ JSON بيقبل أي حاجة

الـ JSON مجرد نص. الـ JSON.parse بيتأكد إن الأقواس والفواصل مظبوطة وبس. مش بيسأل عن المعنى خالص:

  • مفيش types متفق عليها: "30" و30 الاتنين JSON سليم، بس واحد string والتاني number.
  • مفيش حقول إجبارية: {} JSON سليم برضه، حتى لو الكود مستني name وemail.
  • مفيش منع لحقول زيادة: حد يبعتلك "isAdmin": true جوه الـ body، والـ parser هيقبله.
  • مفيش حدود: اسم طوله ١٠ ميجا، أو qty: -5، أو array فيها مليون عنصر.
surprise.js
const body = JSON.parse('{"price": "100", "qty": 2}');console.log(body.price * body.qty); // 200 ← اشتغلت بالصدفةconsole.log(body.price + body.qty); // "1002" ← مصيبةconsole.log(body.discount.value);   // TypeError: Cannot read properties of undefined
الـ JSON السليم syntax مش معناه JSON سليم في المعنى.

طب والـ TypeScript؟

الـ TypeScript بيراجع الكود وقت الكتابة، وبعد الـ compile الـ types كلها بتتمسح. يعني:

types-vanish.ts
interface Order {  price: number;  qty: number;}// الـ TS مصدّقك على كلمتك، ومش بيراجع حاجةconst order = JSON.parse(rawBody) as Order;order.price.toFixed(2); // لو price جت string ← TypeError وقت التشغيل
  • الـ as Order كذبة مهذبة: إنت بتقول للـ compiler “صدقني”، وهو بيصدقك.
  • الـ JSON.parse بيرجّع any: فالـ type safety بتقف عند أول بيانات جاية من بره.
  • الـ types مش موجودة في الـ JavaScript النهائي: مفيش حاجة اسمها Order وقت التشغيل عشان تراجع عليها.

حدود الثقة (trust boundaries)

أي مكان البيانات بتدخل فيه سيستمك من مصدر إنت مش متحكم فيه 100% هو trust boundary. هنا لازم تراجع:

كل الأبواب بتعدي على نفس البواب
  1. API body ← Validator
  2. Webhook ← Validator
  3. Config file ← Validator
  4. LLM output ← Validator
  5. Validator ← الكود بتاعك: valid
  6. Validator ← 400 / reject: invalid
  • الـ API requests: الـ body والـ query والـ headers. أي حد يقدر يبعت أي حاجة بـ curl.
  • الـ webhooks: service تانية بتبعتلك events. ممكن تغيّر شكلها في version جديد من غير ما تاخد بالك.
  • ملفات الـ config: يوزر بيكتب "port": "3000" أو بيغلط في اسم key.
  • رسايل الـ queue: service قديمة لسه بتبعت الشكل القديم في Kafka أو RabbitMQ.
  • ردود الـ LLM: الموديل ممكن يرجّع field ناقص أو type غلط حتى لو طلبت منه JSON.
  • بيانات قديمة في الداتابيز: document في MongoDB اتكتب من سنتين بشكل مختلف.
  • الـ localStorage والـ URL params: على الـ frontend، أي حاجة اليوزر يقدر يعدّلها.

Fail fast: ارفض عند الباب

لو البيانات الغلط عدّت من الباب، الغلط بيظهر بعيد عن سببه:

  • ارفض بدري: الـ request يرجع 400 ومعاه رسالة بتقول /qty must be integer.
  • الغلط قريب من سببه: اللي بعت الغلط هو اللي بيعرف على طول، مش إنت بعد أسبوع.
  • الكود اللي جوه يبقى أبسط: مش محتاج if (typeof x === "number") في كل function، لأن الباب ضمنلك الشكل.

الحلول اللي قدامك

if يدوي

if (typeof body.name !== "string") في كل حتة. بيشتغل لأول ٣ حقول، وبعدين بيبقى كود طويل ومتكرر وناقص، ومحدش بيكتب رسالة error كويسة.

مكتبة في لغة واحدة

زي Zod أو Joi أو Yup. ممتازة جوه TypeScript/JavaScript، بس الـ schema كود، فمش هتشاركها مع service مكتوبة بـ Python أو Go بسهولة.

JSON Schema

الـ schema نفسها JSON. أي لغة تقراها، وفيه validators لكل اللغات تقريبًا. وبتستخدمها OpenAPI والـ editors والـ LLM APIs وMongoDB.

Protobuf / Avro

formats binary ليها schema إجبارية. ممتازة بين الـ services الداخلية، بس مش JSON، والـ browser والـ APIs العامة غالبًا عايزين JSON.

خلّصت الفصل لما

🧠 اختبر نفسك
١) الـ API بتاعك مكتوب بـ TypeScript strict. محتاج validation للـ body؟
آه. الـ types بتتمسح بعد الـ compile، وreq.body جاي من بره ومش مضمون. الـ TypeScript مش بيشوف البيانات وقت التشغيل.
٢) ليه أرفض الـ request عند الباب بدل ما أصلّح البيانات جوه؟
عشان الغلط يظهر قريب من سببه، واللي بعته يعرف على طول، والكود الداخلي يفضل بسيط وواثق في شكل البيانات.
٣) ميزة JSON Schema الأهم عن Zod؟
إنها JSON مش كود، فتتشارك بين أي لغات، وبتستخدمها أدوات كتير زي OpenAPI والـ editors والـ LLM APIs.