# 

# 01-4. 請求體與回應模型

&gt; ⏱️ **閱讀時間：** 15 分鐘
&gt; 🎯 **難度：** ⭐⭐ (基礎)

---

## 🤔 一句話解釋

**請求體是客戶端發送給 API 的資料，回應模型定義 API 返回的資料格式。**

```
Client                              Server
  │                                    │
  │  POST /users                       │
  │  ─────────────────────────────────▶│
  │  請求體 (Request Body):            │
  │  {&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;...&#34;}  │
  │                                    │
  │  回應 (Response):                  │
  │  ◀─────────────────────────────────│
  │  {&#34;id&#34;: 1, &#34;name&#34;: &#34;John&#34;, ...}    │
  │                                    │
```

---

## 📥 請求體 (Request Body)

### 基本用法

使用 Pydantic 模型定義請求體結構：

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 定義請求體模型
class UserCreate(BaseModel):
    name: str
    email: str
    age: int

@app.post(&#34;/users&#34;)
async def create_user(user: UserCreate):
    # user 自動被驗證和轉換
    return {
        &#34;message&#34;: f&#34;使用者 {user.name} 建立成功&#34;,
        &#34;user&#34;: user
    }
```

**請求：**

```bash
curl -X POST &#34;http://localhost:8000/users&#34; \
     -H &#34;Content-Type: application/json&#34; \
     -d &#39;{&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;, &#34;age&#34;: 25}&#39;
```

**回應：**

```json
{
    &#34;message&#34;: &#34;使用者 John 建立成功&#34;,
    &#34;user&#34;: {
        &#34;name&#34;: &#34;John&#34;,
        &#34;email&#34;: &#34;john@example.com&#34;,
        &#34;age&#34;: 25
    }
}
```

### 帶預設值的欄位

```python
from pydantic import BaseModel
from typing import Optional

class ItemCreate(BaseModel):
    name: str                           # 必填
    description: Optional[str] = None   # 選填，預設 None
    price: float                        # 必填
    quantity: int = 1                   # 選填，預設 1
    is_active: bool = True              # 選填，預設 True

@app.post(&#34;/items&#34;)
async def create_item(item: ItemCreate):
    return item
```

**最小請求：**

```json
{
    &#34;name&#34;: &#34;筆記本&#34;,
    &#34;price&#34;: 99.9
}
```

**完整請求：**

```json
{
    &#34;name&#34;: &#34;筆記本&#34;,
    &#34;description&#34;: &#34;A5 大小，200頁&#34;,
    &#34;price&#34;: 99.9,
    &#34;quantity&#34;: 10,
    &#34;is_active&#34;: true
}
```

### 巢狀模型

```python
from pydantic import BaseModel
from typing import Optional, List

class Address(BaseModel):
    city: str
    street: str
    zip_code: str

class Contact(BaseModel):
    phone: Optional[str] = None
    email: str

class UserCreate(BaseModel):
    name: str
    address: Address              # 巢狀模型
    contacts: List[Contact]       # 模型列表

@app.post(&#34;/users&#34;)
async def create_user(user: UserCreate):
    return user
```

**請求：**

```json
{
    &#34;name&#34;: &#34;John&#34;,
    &#34;address&#34;: {
        &#34;city&#34;: &#34;台北市&#34;,
        &#34;street&#34;: &#34;信義路100號&#34;,
        &#34;zip_code&#34;: &#34;110&#34;
    },
    &#34;contacts&#34;: [
        {&#34;email&#34;: &#34;john@work.com&#34;, &#34;phone&#34;: &#34;0912345678&#34;},
        {&#34;email&#34;: &#34;john@personal.com&#34;}
    ]
}
```

---

## 📤 回應模型 (Response Model)

### 為什麼需要回應模型？

```python
# ❌ 不好的做法：直接回傳資料庫模型
@app.get(&#34;/users/{user_id}&#34;)
async def get_user(user_id: int):
    user = database.get_user(user_id)
    return user  # 可能包含密碼等敏感資訊！

# ✅ 好的做法：使用回應模型過濾
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    # 沒有 password！

@app.get(&#34;/users/{user_id}&#34;, response_model=UserResponse)
async def get_user(user_id: int):
    user = database.get_user(user_id)
    return user  # 自動過濾，只返回指定欄位
```

### 基本用法

```python
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional

app = FastAPI()

# 請求模型（建立時用）
class UserCreate(BaseModel):
    name: str
    email: EmailStr
    password: str  # 請求時需要密碼

# 回應模型（回傳時用）
class UserResponse(BaseModel):
    id: int
    name: str
    email: EmailStr
    created_at: datetime
    # 沒有 password！不會回傳給客戶端

@app.post(&#34;/users&#34;, response_model=UserResponse)
async def create_user(user: UserCreate):
    # 模擬建立使用者
    new_user = {
        &#34;id&#34;: 1,
        &#34;name&#34;: user.name,
        &#34;email&#34;: user.email,
        &#34;password&#34;: &#34;hashed_password&#34;,  # 這個不會被回傳
        &#34;created_at&#34;: datetime.now()
    }
    return new_user  # FastAPI 會根據 response_model 過濾
```

**請求：**

```json
{
    &#34;name&#34;: &#34;John&#34;,
    &#34;email&#34;: &#34;john@example.com&#34;,
    &#34;password&#34;: &#34;secret123&#34;
}
```

**回應（密碼被過濾）：**

```json
{
    &#34;id&#34;: 1,
    &#34;name&#34;: &#34;John&#34;,
    &#34;email&#34;: &#34;john@example.com&#34;,
    &#34;created_at&#34;: &#34;2025-12-17T10:30:00&#34;
}
```

### 列表回應

```python
from typing import List

class ItemResponse(BaseModel):
    id: int
    name: str
    price: float

@app.get(&#34;/items&#34;, response_model=List[ItemResponse])
async def list_items():
    items = [
        {&#34;id&#34;: 1, &#34;name&#34;: &#34;Item 1&#34;, &#34;price&#34;: 10.0, &#34;secret&#34;: &#34;xxx&#34;},
        {&#34;id&#34;: 2, &#34;name&#34;: &#34;Item 2&#34;, &#34;price&#34;: 20.0, &#34;secret&#34;: &#34;yyy&#34;},
    ]
    return items  # secret 欄位會被過濾
```

### response_model 進階選項

```python
from pydantic import BaseModel
from typing import Optional

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# 排除未設定的欄位
@app.get(
    &#34;/items/{item_id}&#34;,
    response_model=Item,
    response_model_exclude_unset=True  # 不回傳 None 的欄位
)
async def get_item(item_id: int):
    return {&#34;name&#34;: &#34;Item&#34;, &#34;price&#34;: 10.0}
    # 回應: {&#34;name&#34;: &#34;Item&#34;, &#34;price&#34;: 10.0}
    # 不會包含 description 和 tax

# 排除特定欄位
@app.get(
    &#34;/items-no-tax/{item_id}&#34;,
    response_model=Item,
    response_model_exclude={&#34;tax&#34;}  # 排除 tax
)
async def get_item_no_tax(item_id: int):
    return {&#34;name&#34;: &#34;Item&#34;, &#34;price&#34;: 10.0, &#34;tax&#34;: 1.0}
    # 回應不會包含 tax

# 只包含特定欄位
@app.get(
    &#34;/items-name/{item_id}&#34;,
    response_model=Item,
    response_model_include={&#34;name&#34;, &#34;price&#34;}  # 只包含這些
)
async def get_item_name(item_id: int):
    return {&#34;name&#34;: &#34;Item&#34;, &#34;price&#34;: 10.0, &#34;tax&#34;: 1.0}
```

---

## 🎨 請求體 &#43; 路徑參數 &#43; 查詢參數

可以同時使用多種參數來源：

```python
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class ItemUpdate(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None
    description: Optional[str] = None

@app.put(&#34;/items/{item_id}&#34;)
async def update_item(
    # 路徑參數
    item_id: int = Path(..., ge=1, description=&#34;項目 ID&#34;),

    # 查詢參數
    notify: bool = Query(False, description=&#34;是否發送通知&#34;),

    # 請求體
    item: ItemUpdate = None
):
    result = {
        &#34;item_id&#34;: item_id,
        &#34;notify&#34;: notify,
        &#34;updated_fields&#34;: item.model_dump(exclude_unset=True) if item else {}
    }
    return result
```

**請求：**

```bash
curl -X PUT &#34;http://localhost:8000/items/123?notify=true&#34; \
     -H &#34;Content-Type: application/json&#34; \
     -d &#39;{&#34;name&#34;: &#34;新名稱&#34;, &#34;price&#34;: 199.99}&#39;
```

**回應：**

```json
{
    &#34;item_id&#34;: 123,
    &#34;notify&#34;: true,
    &#34;updated_fields&#34;: {
        &#34;name&#34;: &#34;新名稱&#34;,
        &#34;price&#34;: 199.99
    }
}
```

---

## 🔄 多個請求體

有時候需要接收多個不同的請求體：

```python
from fastapi import FastAPI, Body
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

class User(BaseModel):
    username: str

@app.put(&#34;/items/{item_id}&#34;)
async def update_item(
    item_id: int,
    item: Item,
    user: User,
    importance: int = Body(..., ge=1, le=5)  # 單一值也可以放在 body
):
    return {
        &#34;item_id&#34;: item_id,
        &#34;item&#34;: item,
        &#34;user&#34;: user,
        &#34;importance&#34;: importance
    }
```

**請求：**

```json
{
    &#34;item&#34;: {
        &#34;name&#34;: &#34;Foo&#34;,
        &#34;price&#34;: 50.5
    },
    &#34;user&#34;: {
        &#34;username&#34;: &#34;john&#34;
    },
    &#34;importance&#34;: 5
}
```

### 使用 Body(embed=True)

```python
class Item(BaseModel):
    name: str
    price: float

# 不使用 embed
@app.post(&#34;/items&#34;)
async def create_item(item: Item):
    pass
# 請求體: {&#34;name&#34;: &#34;Foo&#34;, &#34;price&#34;: 50.5}

# 使用 embed
@app.post(&#34;/items-embedded&#34;)
async def create_item_embedded(item: Item = Body(embed=True)):
    pass
# 請求體: {&#34;item&#34;: {&#34;name&#34;: &#34;Foo&#34;, &#34;price&#34;: 50.5}}
```

---

## 📝 實戰範例：部落格文章 API

```python
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
from datetime import datetime
from enum import Enum

app = FastAPI(title=&#34;部落格 API&#34;)

# ===== Enums =====
class PostStatus(str, Enum):
    draft = &#34;draft&#34;
    published = &#34;published&#34;
    archived = &#34;archived&#34;

# ===== 請求模型 =====
class TagCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=30)

class PostCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    content: str = Field(..., min_length=10)
    summary: Optional[str] = Field(None, max_length=500)
    tags: List[str] = Field(default_factory=list)
    status: PostStatus = PostStatus.draft

class PostUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    content: Optional[str] = Field(None, min_length=10)
    summary: Optional[str] = Field(None, max_length=500)
    tags: Optional[List[str]] = None
    status: Optional[PostStatus] = None

# ===== 回應模型 =====
class AuthorResponse(BaseModel):
    id: int
    name: str
    avatar_url: Optional[str] = None

class PostResponse(BaseModel):
    id: int
    title: str
    content: str
    summary: Optional[str]
    tags: List[str]
    status: PostStatus
    author: AuthorResponse
    view_count: int
    created_at: datetime
    updated_at: Optional[datetime]

    model_config = ConfigDict(from_attributes=True)

class PostListResponse(BaseModel):
    &#34;&#34;&#34;文章列表回應（不含完整內容）&#34;&#34;&#34;
    id: int
    title: str
    summary: Optional[str]
    tags: List[str]
    status: PostStatus
    author: AuthorResponse
    view_count: int
    created_at: datetime

class PaginatedResponse(BaseModel):
    &#34;&#34;&#34;分頁回應&#34;&#34;&#34;
    total: int
    page: int
    per_page: int
    pages: int
    items: List[PostListResponse]

# ===== 模擬資料 =====
fake_author = AuthorResponse(id=1, name=&#34;John Doe&#34;, avatar_url=None)
fake_posts = {}
post_counter = 0

# ===== API 端點 =====
@app.get(&#34;/posts&#34;, response_model=PaginatedResponse)
async def list_posts(
    page: int = Query(1, ge=1),
    per_page: int = Query(10, ge=1, le=100),
    status: Optional[PostStatus] = None,
    tag: Optional[str] = None,
    search: Optional[str] = Query(None, min_length=2)
):
    &#34;&#34;&#34;
    獲取文章列表

    - 支援分頁
    - 支援狀態過濾
    - 支援標籤過濾
    - 支援關鍵字搜尋
    &#34;&#34;&#34;
    # 過濾邏輯（簡化版）
    filtered_posts = list(fake_posts.values())

    if status:
        filtered_posts = [p for p in filtered_posts if p[&#34;status&#34;] == status]
    if tag:
        filtered_posts = [p for p in filtered_posts if tag in p[&#34;tags&#34;]]
    if search:
        filtered_posts = [p for p in filtered_posts if search.lower() in p[&#34;title&#34;].lower()]

    total = len(filtered_posts)
    start = (page - 1) * per_page
    end = start &#43; per_page
    paginated = filtered_posts[start:end]

    return PaginatedResponse(
        total=total,
        page=page,
        per_page=per_page,
        pages=(total &#43; per_page - 1) // per_page,
        items=[PostListResponse(**p, author=fake_author) for p in paginated]
    )

@app.post(&#34;/posts&#34;, response_model=PostResponse, status_code=201)
async def create_post(post: PostCreate):
    &#34;&#34;&#34;建立新文章&#34;&#34;&#34;
    global post_counter
    post_counter &#43;= 1

    new_post = {
        &#34;id&#34;: post_counter,
        &#34;title&#34;: post.title,
        &#34;content&#34;: post.content,
        &#34;summary&#34;: post.summary,
        &#34;tags&#34;: post.tags,
        &#34;status&#34;: post.status,
        &#34;view_count&#34;: 0,
        &#34;created_at&#34;: datetime.now(),
        &#34;updated_at&#34;: None
    }
    fake_posts[post_counter] = new_post

    return PostResponse(**new_post, author=fake_author)

@app.get(&#34;/posts/{post_id}&#34;, response_model=PostResponse)
async def get_post(
    post_id: int = Path(..., ge=1)
):
    &#34;&#34;&#34;獲取單一文章&#34;&#34;&#34;
    if post_id not in fake_posts:
        raise HTTPException(status_code=404, detail=&#34;文章不存在&#34;)

    post = fake_posts[post_id]
    post[&#34;view_count&#34;] &#43;= 1  # 增加瀏覽次數

    return PostResponse(**post, author=fake_author)

@app.patch(&#34;/posts/{post_id}&#34;, response_model=PostResponse)
async def update_post(
    post_id: int = Path(..., ge=1),
    post_update: PostUpdate = None
):
    &#34;&#34;&#34;更新文章（部分更新）&#34;&#34;&#34;
    if post_id not in fake_posts:
        raise HTTPException(status_code=404, detail=&#34;文章不存在&#34;)

    post = fake_posts[post_id]

    if post_update:
        update_data = post_update.model_dump(exclude_unset=True)
        for key, value in update_data.items():
            post[key] = value
        post[&#34;updated_at&#34;] = datetime.now()

    return PostResponse(**post, author=fake_author)

@app.delete(&#34;/posts/{post_id}&#34;, status_code=204)
async def delete_post(
    post_id: int = Path(..., ge=1)
):
    &#34;&#34;&#34;刪除文章&#34;&#34;&#34;
    if post_id not in fake_posts:
        raise HTTPException(status_code=404, detail=&#34;文章不存在&#34;)

    del fake_posts[post_id]
```

---

## ✅ 重點總結

### 請求體 vs 回應模型

| 特性 | 請求體 (Request Body) | 回應模型 (Response Model) |
|------|---------------------|-------------------------|
| 用途 | 接收客戶端資料 | 過濾回傳資料 |
| 定義方式 | 參數型別為 Pydantic 模型 | `response_model=` |
| 驗證 | 自動驗證輸入 | 自動過濾輸出 |
| 常見場景 | POST/PUT/PATCH | 所有方法 |

### 常見模式

```python
# 1. 建立用模型（接收所有資料）
class UserCreate(BaseModel):
    name: str
    email: str
    password: str  # 敏感資料

# 2. 回應用模型（過濾敏感資料）
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    # 沒有 password

# 3. 更新用模型（全部選填）
class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[str] = None
    password: Optional[str] = None
```

### 最佳實踐

1. **分離請求和回應模型** - 不要用同一個模型
2. **回應模型不包含敏感資料** - 密碼、token 等
3. **使用 `response_model`** - 確保回應格式一致
4. **更新用模型欄位設為選填** - 支援部分更新

---

## 🎤 面試這樣答

### Q: FastAPI 中如何確保 API 不會回傳敏感資料？

**答案：**

&gt; 使用 `response_model` 參數指定回應模型，FastAPI 會自動過濾掉不在模型中的欄位。
&gt;
&gt; ```python
&gt; class UserResponse(BaseModel):
&gt;     id: int
&gt;     name: str
&gt;     # 沒有 password
&gt;
&gt; @app.get(&#34;/users/{id}&#34;, response_model=UserResponse)
&gt; async def get_user(id: int):
&gt;     user = db.get_user(id)  # 可能包含 password
&gt;     return user  # 自動過濾，不會回傳 password
&gt; ```
&gt;
&gt; 這樣即使資料庫模型包含敏感欄位，回應中也不會出現。

---

## 🤓 小測驗

1. 如何定義一個請求體？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   用 Pydantic 的 BaseModel 定義，然後作為函數參數的型別
   &lt;/details&gt;

2. `response_model` 的作用是什麼？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   定義回應的格式，自動過濾不在模型中的欄位
   &lt;/details&gt;

3. 如何讓回應不包含值為 None 的欄位？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用 response_model_exclude_unset=True
   &lt;/details&gt;

---

**上一篇：** [01-3. 路徑參數與查詢參數](./01-3)
**下一篇：** [01-5. 狀態碼與錯誤處理](./01-5)

---

最後更新：2025-12-17


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/fastapi/01-4/  

