نموذج الاستجابة — التحكم الدقيق في مخرجات API
نموذج الاستجابة كإضاءة المسرح — يُنير فقط المناطق المخصصة للجمهور، بينما يبقي معدات الكواليس (الحقول الداخلية) في الظلام، مما يضمن الجمال والأمان معاً.
1. ما ستتعلمه
- أساسيات
response_model: التصفية التلقائية للحقول غير المُعلَنة، والتحكم في serialization response_model_exclude/response_model_include: تصفية دقيقة على مستوى الحقل- نموذج الاستجابة المتعدد: نقطة نهاية واحدة تُرجع نماذج مختلفة (استجابة
Union/قائمة) response_model_by_aliasوresponse_model_exclude_unset: نصائح عملية- سيناريو Alice: استجابة سعر PriceTracker — النسخة العامة (سعر التكلفة مخفي) مقابل نسخة المشرف (سلسلة أسعار كاملة)
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: تسرب أسعار التكلفة للمنافسين
يخزّن PriceTracker الخاص بـ Alice أسعار التجزئة والجملة للمنتجات. في أحد الأيام، عندما طلب Bob تفاصيل المنتج عبر الواجهة الأمامية، أعادت API أسعار الجملة أيضاً. قام منافس بالاستخراج الآلي للـ API وحصل على جميع معلومات أسعار الجملة، مما أثار استياء شديداً من عملاء Alice. السبب الجذري للمشكلة هو أن Flask يفتقر إلى آلية تصفية الاستجابة، وجميع حقول كائن ORM تُسلسَل وتُعاد مباشرة.
(2) حل response_model
التصفية التصريحية لـ response_model في FastAPI — تُرجع فقط الحقول المُعلَنة في النموذج، وتستبعد تلقائياً الحقول غير المُعلَنة، وتمنع تسريب البيانات على مستوى المخطط.
from fastapi import FastAPI
from pydantic import BaseModel
class ProductPublic(BaseModel):
id: int
name: str
retail_price: float # سعر التجزئة فقط
class ProductAdmin(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float # يتضمن سعر الجملة
app = FastAPI()
@app.get("/products/{id}", response_model=ProductPublic)
async def get_product_public(id: int):
# حتى لو كانت قاعدة البيانات تحتوي على wholesale_price، فإن response_model يصفّيها
return {"id": id, "name": "Widget", "retail_price": 29.99, "wholesale_price": 15.0}
(3) العائد
تحوّلت تصفية الاستجابة من "تذكّر حذف الحقول يدوياً" إلى "تصفية تلقائية بناءً على النماذج المُعلَنة"، مما يقضي تماماً على مشكلة تسريب أسعار الجملة. API العامة الآن تُرجع فقط الحقول المُعلَنة في ProductPublic، بينما API الإدارة تُرجع البيانات الكاملة باستخدام ProductAdmin.
3. أساسيات response_model
(1) مبادئ التصفية التلقائية
flowchart LR
A[كائن ORM] --> B{response_model}
B -->|حقول مُعلَنة| C[استجابة JSON]
B -->|حقول غير مُعلَنة| D[مُصفَّاة]
subgraph ProductPublic
E[id]
F[name]
G[retail_price]
end
subgraph Hidden
H[wholesale_price]
I[cost_price]
J[supplier_id]
end
(1) ▶ مثال: التصفية التلقائية باستخدام response_model
from fastapi import FastAPI
from pydantic import BaseModel
class ProductResponse(BaseModel):
id: int
name: str
category: str
# سجل قاعدة بيانات محاكى بحقول إضافية
DB_PRODUCT = {
"id": 1,
"name": "Widget",
"category": "electronics",
"cost_price": 8.50, # يجب ألا يكون في الاستجابة
"supplier_id": 42, # يجب ألا يكون في الاستجابة
}
app = FastAPI()
@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
# cost_price وsupplier_id تُصفَّيان تلقائياً
return DB_PRODUCT
Output:
{"id": 1, "name": "Widget", "category": "electronics"}
(2) مقارنة بين response_model وأنواع الإرجاع
| الطريقة | سلوك التصفية | توليد التوثيق | السيناريوهات الموصى بها |
|---|---|---|---|
response_model=X |
تصفية تلقائية | نعم | يُوصى بها دائماً |
نوع الإرجاع -> X |
بدون تصفية | نعم | تعليق توضيحي للنوع فقط |
| بدون تعريف | بدون تصفية | لا | غير موصى بها |
(2) ▶ مثال: response_model مقابل نوع الإرجاع
from fastapi import FastAPI
from pydantic import BaseModel
class ProductBrief(BaseModel):
id: int
name: str
app = FastAPI()
@app.get("/demo/model", response_model=ProductBrief)
async def with_response_model():
# response_model يُصفّي: فقط id وname في الاستجابة
return {"id": 1, "name": "Widget", "secret": "hidden"}
@app.get("/demo/type") -> ProductBrief
async def with_return_type():
# نوع الإرجاع لا يُصفّي: حقل secret يتسرّب!
return {"id": 1, "name": "Widget", "secret": "leaked"}
الناتج:
# تم تعريف الدالة بنجاح
4. تصفية دقيقة على مستوى الحقل
(1) exclude وinclude
عندما لا ترغب في إنشاء نموذج جديد لكل سيناريو، يمكنك استخدام response_model_exclude وresponse_model_include لتصفية الحقول ديناميكياً.
(1) ▶ مثال: exclude لاستبعاد الحقول الحساسة
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float = Field(exclude=True) # استبعاد افتراضي
cost_price: float = Field(exclude=True)
supplier: str = Field(exclude=True)
app = FastAPI()
@app.get("/products/{id}", response_model=ProductFull)
async def get_product(id: int):
return {
"id": id,
"name": "Widget",
"retail_price": 29.99,
"wholesale_price": 15.0,
"cost_price": 8.50,
"supplier": "Acme Corp",
}
الناتج:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (تم تصفية wholesale_price وcost_price وsupplier):
{"id": 1, "name": "Widget", "retail_price": 29.99}
(2) ▶ مثال: response_model_exclude للاستبعاد الديناميكي
from fastapi import FastAPI
from pydantic import BaseModel
class ProductFull(BaseModel):
id: int
name: str
retail_price: float
wholesale_price: float
cost_price: float
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductFull,
response_model_exclude={"wholesale_price", "cost_price"},
)
async def get_product_public(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
@app.get(
"/admin/products/{id}",
response_model=ProductFull,
)
async def get_product_admin(id: int):
return {
"id": id, "name": "Widget",
"retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
}
الناتج:
# تم تعريف الدالة بنجاح
| طريقة التصفية | السيناريوهات المطبَّقة | الدقة |
|---|---|---|
response_model=ModelA |
نماذج مختلفة من زوايا مختلفة | مستوى النموذج |
response_model_exclude={fields} |
استبعاد عدد قليل من الحقول | مستوى الحقل |
response_model_include={fields} |
تضمين عدد قليل من الحقول فقط | مستوى الحقل |
Field(exclude=True) |
استبعاد حقل محدد | مستوى تعريف الحقل |
5. نماذج الاستجابة المتعددة والتقنيات المتقدمة
(1) استجابات مختلفة لنفس نقطة النهاية
(1) ▶ مثال: نموذج استجابة Union
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
class ProductFound(BaseModel):
id: int
name: str
price: float
class ProductNotFound(BaseModel):
error: str
product_id: int
app = FastAPI()
@app.get("/products/{id}", response_model=Union[ProductFound, ProductNotFound])
async def search_product(id: int):
if id == 1:
return ProductFound(id=1, name="Widget", price=9.99)
return ProductNotFound(error="Not found", product_id=id)
الناتج:
# تم تعريف الدالة بنجاح
(2) ▶ مثال: نموذج استجابة القائمة
from fastapi import FastAPI
from pydantic import BaseModel
class PriceEntry(BaseModel):
product_id: int
price: float
recorded_at: str
app = FastAPI()
@app.get("/products/{id}/prices", response_model=list[PriceEntry])
async def get_price_history(id: int):
return [
{"product_id": id, "price": 29.99, "recorded_at": "2026-01-01"},
{"product_id": id, "price": 24.99, "recorded_at": "2026-02-01"},
{"product_id": id, "price": 34.99, "recorded_at": "2026-03-01"},
]
الناتج:
# تم تعريف الدالة بنجاح
(2) exclude_unset وexclude_none
(3) ▶ مثال: exclude_unset يُرجع فقط الحقول ذات القيم
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
class ProductUpdate(BaseModel):
name: Optional[str] = None
category: Optional[str] = None
price: Optional[float] = None
app = FastAPI()
@app.get(
"/products/{id}",
response_model=ProductUpdate,
response_model_exclude_unset=True,
)
async def get_product_partial(id: int):
# فقط name تم تعيينه، category وprice يبقيان None
return ProductUpdate(name="Updated Widget")
الناتج:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (يتضمن فقط الحقول ذات القيم):
{"name": "Updated Widget"}
| الخيار | التأثير | حالات الاستخدام |
|---|---|---|
response_model_exclude_unset=True |
استبعاد الحقول غير المعيَّنة | استجابة PATCH: تُرجع فقط الحقول المحدَّثة |
response_model_exclude_none=True |
استبعاد الحقول ذات القيمة None | تبسيط الاستجابة بإزالة القيم الفارغة |
response_model_exclude_defaults=True |
استبعاد الحقول التي تستخدم القيم الافتراضية | إزالة القيم الافتراضية المتكررة |
response_model_by_alias=True |
إخراج الحقول باستخدام الأسماء المستعارة | الواجهة الأمامية تتطلب تسمية camelCase |
(4) ▶ مثال: response_model_by_alias
from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict
class ProductResponse(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: int
product_name: str = Field(alias="productName")
unit_price: float = Field(alias="unitPrice", description="السعر بالدولار")
app = FastAPI()
@app.get("/products/{id}", response_model=ProductResponse, response_model_by_alias=True)
async def get_product(id: int):
return {"id": id, "productName": "Widget", "unitPrice": 9.99}
الناتج:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (استخدام الأسماء المستعارة لأسماء الحقول):
{"id": 1, "productName": "Widget", "unitPrice": 9.99}
❓أسئلة شائعة
response_model وتعليق نوع الإرجاع؟response_model الحقول غير المُعلَنة ويتحقق من المخرجات، بينما تعليق نوع الإرجاع يؤثر فقط على توليد توثيق OpenAPI ولا يُصفّي الحقول. استخدم response_model دائماً.exclude_unset أكثر فائدة؟{"nested_model": {"secret_field"}}.response_model=list[ItemModel]، وسيُصفّي FastAPI تلقائياً كل عنصر في القائمة وفقاً للنموذج.response_model على الأداء؟orjson لتحسين serialization.📖ملخص
- يُصفّي
response_modelتلقائياً الحقول غير المُعلَنة لمنع تسريب البيانات الحساسة على مستوى المخطط - يوفر
response_model_exclude/response_model_includeتحكماً دقيقاً على مستوى الحقل دون الحاجة لإنشاء نموذج جديد - نموذج استجابة
Unionيدعم إرجاع هياكل مختلفة من نفس نقطة النهاية؛ راجعlist[Model]لاستجابات القائمة response_model_exclude_unsetيُرجع فقط الحقول ذات القيم المعيَّنة؛ مناسب لسيناريوهات PATCHresponse_model_by_alias=Trueيستخدم الأسماء المستعارة للاستجابات لتلبية اصطلاحات تسمية camelCase للواجهة الأمامية
📝تمارين
- مسألة أساسية (الصعوبة ⭐): أنشئ نموذج
ProductPublic(id، name، price)، واستخدمresponse_modelلضمان أن نقطة النهاية تُرجع فقط هذه الحقول الثلاثة — حتى لو أرجعت قاموساً يحتوي علىcost_price، لا تتسرب معلومات. تلميح:@app.get(..., response_model=ProductPublic) - تمرين متقدم (الصعوبة ⭐⭐): أنشئ نقطتي نهاية لـ PriceTracker — نقطة نهاية عامة تخفي
wholesale_price، ونقطة نهاية مشرف تعرض جميع الحقول. نفّذ ذلك باستخدامresponse_model_exclude. تلميح:response_model_exclude={"wholesale_price"} - تحدٍ (الصعوبة ⭐⭐⭐): أنشئ نموذج
ProductDetailبحقول اختيارية (مثل description، image_url، إلخ)، واستخدمresponse_model_exclude_unset=Trueلضمان أن نقطة النهاية تُرجع فقط الحقول التي قدّمها العميل، مع حذف القيم الفارغة من JSON. تلميح:Optional[str] = None+response_model_exclude_unset=True
---|



