انتقل إلى المحتوى الرئيسي

الوحدة 2 — التحقّق من المدخلات مع Pydantic

في الوحدة السابقة رأينا أنّ FastAPI يبني توثيقًا آليًّا من تلميحات الأنواع. القيمة الحقيقيّة تظهر عندما نُصرّح بمدخلاتنا كأصناف Pydantic كاملة. عندها يصبح لكلّ حقل نوع، ونطاق، وقيمة افتراضيّة اختياريّة، ومثال يظهر في التوثيق التفاعليّ. أيّ خرق يُعيد استجابة 422 مُهيكلة تشرح الحقل الخاطئ للعميل، من دون سطر واحد للتحقّق مكتوب باليد.

الطلب: مخطّط ChurnRequest

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

from typing import Literal
from pydantic import BaseModel, Field


class ChurnRequest(BaseModel):
tenure_months: int = Field(
...,
ge=0,
le=120,
description="مدّة اشتراك العميل بالأشهر (0 إلى 120)",
examples=[24],
)
monthly_charges: float = Field(
...,
gt=0,
le=500,
description="الفاتورة الشهريّة بالدولار",
examples=[79.50],
)
contract: Literal["month-to-month", "one-year", "two-year"] = Field(
..., examples=["month-to-month"]
)
payment_method: Literal[
"electronic-check", "mailed-check", "bank-transfer", "credit-card"
] = Field(..., examples=["electronic-check"])
fiber_optic: bool = Field(..., examples=[True])
online_security: bool = Field(..., examples=[False])

Field(...) مع النقاط الثلاث يعلن أنّ الحقل إلزاميّ؛ حذف الحقل عند الطلب يُعيد 422. ge، le، gt، lt تفرض قيودًا رقميّة. Literal[...] يحصر القيم النصّيّة في قائمة مغلقة، فيمنع خطأ مطبعيّ مثل Monthly بدل month-to-month. examples تضع قيمة نموذجيّة في Swagger UI لتسهيل الاختبار.

الاستجابة: مخطّط ChurnResponse

نبني مخطّطًا مقابلًا للاستجابة يمرّ عبر response_model:

class ChurnResponse(BaseModel):
churn_probability: float = Field(
..., ge=0, le=1, description="احتمال التسرّب بين 0 و1"
)
churn_predicted: bool
model_version: str
request_id: str

استعماله في المسار يضمن أنّ أيّ حقل إضافيّ لم يُصرَّح به لن يخرج في الاستجابة، حتّى إن أرجعت الدالّة قاموسًا يحتوي حقولًا أخرى:

from fastapi import FastAPI

app = FastAPI()


@app.post("/predict", response_model=ChurnResponse)
def predire(req: ChurnRequest) -> dict:
proba = 0.72 # placeholder ; la vraie prediction viendra dans la unite 4
return {
"churn_probability": proba,
"churn_predicted": proba >= 0.5,
"model_version": "churn-lgbm-v1.3.0",
"request_id": "req-000001",
"internal_debug": "ne doit pas sortir", # حذف تلقائيّ
}

internal_debug لن يظهر في الاستجابة لأنّه غير مُصرَّح به في ChurnResponse. هذا يمنع تسرّبًا حسّاسًا شائعًا (مسار تصحيح، معرّف داخليّ) عن طريق الخطأ.

400 مقابل 422: التمييز الجوهريّ

هذه نقطة تربك الجميع في البداية. القاعدة العمليّة:

  • 422 Unprocessable Entity: الطلب صالح نحويًّا (JSON مُهيكل)، لكنّه لا يحترم مخطّط البيانات (نوع خاطئ، حقل مفقود، قيمة خارج النطاق). FastAPI يُعيد 422 تلقائيًّا لكلّ خرق مخطّط Pydantic.
  • 400 Bad Request: الطلب غير مفهوم أصلًا (JSON مكسور، Content-Type خاطئ) أو يخرق قاعدة عمل (business rule) لا يمكن التعبير عنها بالمخطّط.

مثال ملموس: tenure_months: -5 يخرق ge=0 فيُعاد 422 من Pydantic دون كتابة سطر. لكن قاعدة عمل مثل «لا نقبل التنبّؤ لحسابات مُغلقة» تحتاج تحقّقًا داخل الدالّة يُعيد 400:

from fastapi import HTTPException, status


@app.post("/predict", response_model=ChurnResponse)
def predire(req: ChurnRequest) -> ChurnResponse:
if req.contract == "two-year" and req.tenure_months == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="عقد سنتَين مع مدّة اشتراك صفر: حالة غير متّسقة",
)
...

الخلط الشائع: كثيرون يُعيدون 400 لكلّ خطأ، فيفقدون التمييز بين خطأ مخطّطيّ (يعرفه المطوّر تلقائيًّا في العميل) وخطأ عمل (يستدعي رسالة أوضح للمستخدم النهائيّ). Pydantic يوفّر لك 422 مجّانًا؛ لا تُلغِ فائدته.

القيم الافتراضيّة والحقول الاختياريّة

الحقل الاختياريّ يُصرَّح بقيمة افتراضيّة أو بـNone:

from typing import Optional


class ChurnRequestV2(ChurnRequest):
customer_id: Optional[str] = Field(default=None, description="معرّف اختياريّ")
notes: str = Field(default="", max_length=200)

في التوثيق التفاعليّ، الحقول الإلزاميّة تُميَّز بنجمة، وتلك الاختياريّة تظهر بقيمها الافتراضيّة. max_length يفرض حدًّا أعلى على النصوص، مفيد لمنع طلبات ضخمة تُهدر ذاكرة.

المدقّقات المخصّصة

بعض القواعد لا تُعبَّر بـge/le. field_validator يفتح المجال لتحقّق مخصّص:

from pydantic import field_validator


class ChurnRequestV3(ChurnRequest):
@field_validator("monthly_charges")
@classmethod
def coherence_fibre(cls, v, info):
data = info.data
if data.get("fiber_optic") and v < 20:
raise ValueError("فاتورة أقلّ من 20 USD مع خدمة فيبر: غير متّسق")
return v

الرسالة تُدمج تلقائيًّا في استجابة 422، فيرى العميل الحقل والسبب.

توليد أمثلة كاملة

model_config يضع مثالًا كاملًا يظهر في Swagger UI ويُسهّل اختبارًا حقيقيًّا:

class ChurnRequest(BaseModel):
... # الحقول كما سبق

model_config = {
"json_schema_extra": {
"example": {
"tenure_months": 24,
"monthly_charges": 79.50,
"contract": "month-to-month",
"payment_method": "electronic-check",
"fiber_optic": True,
"online_security": False,
}
}
}

الآن أيّ مطوّر يفتح /docs يضغط زرّ «Try it out» وينفّذ طلبًا يعمل من المرّة الأولى، بدل قراءة تسع صفحات توثيق.

الخلاصة

  • مخطّط Pydantic يُغني عن مئات أسطر تحقّق يدويّ عبر Field(...) مع ge، le، Literal، max_length.
  • FastAPI يُعيد 422 تلقائيًّا لكلّ خرق مخطّط، ويوجَّه 400 يدويًّا لأخطاء عمل حقيقيّة.
  • response_model يُصفّي المخرجات إلى الحقول المُصرَّح بها، فيمنع تسرّبًا حسّاسًا صامتًا.
  • field_validator يفتح المجال لتحقّق مخصّص، وjson_schema_extra يضع مثالًا حيًّا يظهر في Swagger UI.

قاعدة عمليّة قبل الانتقال إلى الوحدة التالية: عرّف نموذج المُدخَل ونموذج المُخرَج في وحدة schemas.py منفصلة عن main.py، ثمّ استوردهما. هذا يفصل عقد الواجهة عن منطق التحميل والاستدعاء، ويسمح لاحقًا بتشارك المخطّطات مع مكتبة عميل TypeScript أو Python يُولّدها OpenAPI. أيّ حقل تُضيفه لاحقًا يبدأ من هنا، لا من دالّة النقطة الطرفيّة.