404 Not Found

404 Not Found


nginx

نموذج الاستجابة — التحكم الدقيق في مخرجات API

نموذج الاستجابة كإضاءة المسرح — يُنير فقط المناطق المخصصة للجمهور، بينما يبقي معدات الكواليس (الحقول الداخلية) في الظلام، مما يضمن الجمال والأمان معاً.

1. ما ستتعلمه


2. القصة الحقيقية لـ Alice

(1) نقطة الألم: تسرب أسعار التكلفة للمنافسين

يخزّن PriceTracker الخاص بـ Alice أسعار التجزئة والجملة للمنتجات. في أحد الأيام، عندما طلب Bob تفاصيل المنتج عبر الواجهة الأمامية، أعادت API أسعار الجملة أيضاً. قام منافس بالاستخراج الآلي للـ API وحصل على جميع معلومات أسعار الجملة، مما أثار استياء شديداً من عملاء Alice. السبب الجذري للمشكلة هو أن Flask يفتقر إلى آلية تصفية الاستجابة، وجميع حقول كائن ORM تُسلسَل وتُعاد مباشرة.

(2) حل response_model

التصفية التصريحية لـ response_model في FastAPI — تُرجع فقط الحقول المُعلَنة في النموذج، وتستبعد تلقائياً الحقول غير المُعلَنة، وتمنع تسريب البيانات على مستوى المخطط.

PYTHON
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) مبادئ التصفية التلقائية

100%
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

PYTHON
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:

TEXT
{"id": 1, "name": "Widget", "category": "electronics"}

(2) مقارنة بين response_model وأنواع الإرجاع

الطريقة سلوك التصفية توليد التوثيق السيناريوهات الموصى بها
response_model=X تصفية تلقائية نعم يُوصى بها دائماً
نوع الإرجاع -> X بدون تصفية نعم تعليق توضيحي للنوع فقط
بدون تعريف بدون تصفية لا غير موصى بها

(2) ▶ مثال: response_model مقابل نوع الإرجاع

PYTHON
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"}

الناتج:

TEXT
# تم تعريف الدالة بنجاح

4. تصفية دقيقة على مستوى الحقل

(1) exclude وinclude

عندما لا ترغب في إنشاء نموذج جديد لكل سيناريو، يمكنك استخدام response_model_exclude وresponse_model_include لتصفية الحقول ديناميكياً.

(1) ▶ مثال: exclude لاستبعاد الحقول الحساسة

PYTHON
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",
    }

الناتج:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

Output (تم تصفية wholesale_price وcost_price وsupplier):

TEXT
{"id": 1, "name": "Widget", "retail_price": 29.99}

(2) ▶ مثال: response_model_exclude للاستبعاد الديناميكي

PYTHON
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,
    }

الناتج:

TEXT
# تم تعريف الدالة بنجاح
طريقة التصفية السيناريوهات المطبَّقة الدقة
response_model=ModelA نماذج مختلفة من زوايا مختلفة مستوى النموذج
response_model_exclude={fields} استبعاد عدد قليل من الحقول مستوى الحقل
response_model_include={fields} تضمين عدد قليل من الحقول فقط مستوى الحقل
Field(exclude=True) استبعاد حقل محدد مستوى تعريف الحقل

5. نماذج الاستجابة المتعددة والتقنيات المتقدمة

(1) استجابات مختلفة لنفس نقطة النهاية

(1) ▶ مثال: نموذج استجابة Union

PYTHON
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)

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: نموذج استجابة القائمة

PYTHON
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"},
    ]

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) exclude_unset وexclude_none

(3) ▶ مثال: exclude_unset يُرجع فقط الحقول ذات القيم

PYTHON
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")

الناتج:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

Output (يتضمن فقط الحقول ذات القيم):

TEXT
{"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

PYTHON
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}

الناتج:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

Output (استخدام الأسماء المستعارة لأسماء الحقول):

TEXT
{"id": 1, "productName": "Widget", "unitPrice": 9.99}

❓أسئلة شائعة

س ما الفرق بين response_model وتعليق نوع الإرجاع؟
ج يُصفّي response_model الحقول غير المُعلَنة ويتحقق من المخرجات، بينما تعليق نوع الإرجاع يؤثر فقط على توليد توثيق OpenAPI ولا يُصفّي الحقول. استخدم response_model دائماً.
س هل يمكن استخدام exclude وinclude في نفس الوقت؟
ج لا، لا يمكن استخدامهما معاً. اختر أحدهما: exclude يستبعد الحقول المحددة، بينما include يتضمن فقط الحقول المحددة.
س في أي سيناريوهات يكون exclude_unset أكثر فائدة؟
ج في استجابات طلبات PATCH — عندما يحدّث العميل بعض الحقول فقط، تُرجع الاستجابة فقط الحقول المحدَّثة لتجنب الالتباس.
س هل يمكن استبعاد حقول في نموذج متداخل؟
ج نعم، يمكنك استبعاد الحقول المتداخلة باستخدام مسار مفصول بنقاط {"nested_model": {"secret_field"}}.
س كيف أُعلن عن استجابة قائمة؟
ج استخدم response_model=list[ItemModel]، وسيُصفّي FastAPI تلقائياً كل عنصر في القائمة وفقاً للنموذج.
س هل يؤثر response_model على الأداء؟
ج هناك حمل طفيف (serialization + تصفية)، لكن هذا يضمن أمان البيانات واتساق التوثيق. في سيناريوهات ملايين QPS، يمكنك استخدام orjson لتحسين serialization.

📖ملخص


📝تمارين

  1. مسألة أساسية (الصعوبة ⭐): أنشئ نموذج ProductPublic (id، name، price)، واستخدم response_model لضمان أن نقطة النهاية تُرجع فقط هذه الحقول الثلاثة — حتى لو أرجعت قاموساً يحتوي على cost_price، لا تتسرب معلومات. تلميح: @app.get(..., response_model=ProductPublic)
  2. تمرين متقدم (الصعوبة ⭐⭐): أنشئ نقطتي نهاية لـ PriceTracker — نقطة نهاية عامة تخفي wholesale_price، ونقطة نهاية مشرف تعرض جميع الحقول. نفّذ ذلك باستخدام response_model_exclude. تلميح: response_model_exclude={"wholesale_price"}
  3. تحدٍ (الصعوبة ⭐⭐⭐): أنشئ نموذج ProductDetail بحقول اختيارية (مثل description، image_url، إلخ)، واستخدم response_model_exclude_unset=True لضمان أن نقطة النهاية تُرجع فقط الحقول التي قدّمها العميل، مع حذف القيم الفارغة من JSON. تلميح: Optional[str] = None + response_model_exclude_unset=True

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%