# 

# 02-5. 序列化與反序列化

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

---

## 🤔 一句話解釋

**序列化是把 Python 物件轉成 JSON，反序列化是把 JSON 轉回 Python 物件。**

```
Python 物件  ──序列化──▶  JSON/Dict  ──反序列化──▶  Python 物件
  (User)      model_dump()  {&#34;name&#34;:...}  model_validate()  (User)
```

---

## 📤 序列化（Python → JSON）

### model_dump() - 轉換為字典

```python
from pydantic import BaseModel
from datetime import datetime

class User(BaseModel):
    name: str
    email: str
    age: int
    created_at: datetime

user = User(
    name=&#34;John&#34;,
    email=&#34;john@example.com&#34;,
    age=25,
    created_at=datetime(2025, 1, 1, 12, 0, 0)
)

# 基本轉換
print(user.model_dump())
# {
#     &#39;name&#39;: &#39;John&#39;,
#     &#39;email&#39;: &#39;john@example.com&#39;,
#     &#39;age&#39;: 25,
#     &#39;created_at&#39;: datetime(2025, 1, 1, 12, 0, 0)
# }
```

### model_dump_json() - 轉換為 JSON 字串

```python
# JSON 字串
print(user.model_dump_json())
# &#39;{&#34;name&#34;:&#34;John&#34;,&#34;email&#34;:&#34;john@example.com&#34;,&#34;age&#34;:25,&#34;created_at&#34;:&#34;2025-01-01T12:00:00&#34;}&#39;

# 格式化輸出
print(user.model_dump_json(indent=2))
# {
#   &#34;name&#34;: &#34;John&#34;,
#   &#34;email&#34;: &#34;john@example.com&#34;,
#   &#34;age&#34;: 25,
#   &#34;created_at&#34;: &#34;2025-01-01T12:00:00&#34;
# }
```

### 控制輸出內容

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

class User(BaseModel):
    name: str
    email: str
    age: int = 0
    bio: Optional[str] = None
    is_active: bool = True

user = User(name=&#34;John&#34;, email=&#34;john@example.com&#34;)

# 排除特定欄位
user.model_dump(exclude={&#34;email&#34;})
# {&#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 0, &#39;bio&#39;: None, &#39;is_active&#39;: True}

# 只包含特定欄位
user.model_dump(include={&#34;name&#34;, &#34;email&#34;})
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;}

# 排除預設值
user.model_dump(exclude_defaults=True)
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;}

# 排除未設定的值（未明確傳入的）
user.model_dump(exclude_unset=True)
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;}

# 排除 None
user.model_dump(exclude_none=True)
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;, &#39;age&#39;: 0, &#39;is_active&#39;: True}
```

### 巢狀排除

```python
from pydantic import BaseModel

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

class User(BaseModel):
    name: str
    email: str
    address: Address

user = User(
    name=&#34;John&#34;,
    email=&#34;john@example.com&#34;,
    address=Address(city=&#34;Taipei&#34;, street=&#34;Main St&#34;, zip_code=&#34;100&#34;)
)

# 排除巢狀欄位
user.model_dump(exclude={&#34;address&#34;: {&#34;zip_code&#34;}})
# {
#     &#39;name&#39;: &#39;John&#39;,
#     &#39;email&#39;: &#39;john@example.com&#39;,
#     &#39;address&#39;: {&#39;city&#39;: &#39;Taipei&#39;, &#39;street&#39;: &#39;Main St&#39;}
# }

# 排除整個巢狀模型
user.model_dump(exclude={&#34;address&#34;: True})
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;}
```

---

## 📥 反序列化（JSON → Python）

### model_validate() - 從字典建立

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str
    age: int

# 從字典建立
data = {&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;, &#34;age&#34;: 25}
user = User.model_validate(data)

# 等同於
user = User(**data)
```

### model_validate_json() - 從 JSON 字串建立

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str
    age: int

# 從 JSON 字串建立
json_str = &#39;{&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;, &#34;age&#34;: 25}&#39;
user = User.model_validate_json(json_str)
```

### 嚴格模式

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

# 一般模式：會自動轉換型別
data = {&#34;name&#34;: &#34;John&#34;, &#34;age&#34;: &#34;25&#34;}  # age 是字串
user = User.model_validate(data)      # ✅ age 會轉成 int

# 嚴格模式：不自動轉換
user = User.model_validate(data, strict=True)  # ❌ ValidationError
```

---

## 🔧 自訂序列化

### 使用 field_serializer

```python
from pydantic import BaseModel, field_serializer
from datetime import datetime

class User(BaseModel):
    name: str
    created_at: datetime
    balance: float

    @field_serializer(&#39;created_at&#39;)
    def serialize_datetime(self, value: datetime) -&gt; str:
        return value.strftime(&#34;%Y/%m/%d %H:%M&#34;)

    @field_serializer(&#39;balance&#39;)
    def serialize_balance(self, value: float) -&gt; str:
        return f&#34;${value:,.2f}&#34;

user = User(
    name=&#34;John&#34;,
    created_at=datetime(2025, 1, 1, 12, 30),
    balance=1234.5
)

print(user.model_dump())
# {
#     &#39;name&#39;: &#39;John&#39;,
#     &#39;created_at&#39;: &#39;2025/01/01 12:30&#39;,
#     &#39;balance&#39;: &#39;$1,234.50&#39;
# }
```

### 條件序列化

```python
from pydantic import BaseModel, field_serializer, SerializationInfo

class User(BaseModel):
    name: str
    email: str
    phone: str

    @field_serializer(&#39;email&#39;)
    def mask_email(self, value: str, info: SerializationInfo) -&gt; str:
        # 根據 context 決定是否遮罩
        if info.context and info.context.get(&#39;mask_sensitive&#39;):
            parts = value.split(&#39;@&#39;)
            if len(parts) == 2:
                return f&#34;{parts[0][:2]}***@{parts[1]}&#34;
        return value

    @field_serializer(&#39;phone&#39;)
    def mask_phone(self, value: str, info: SerializationInfo) -&gt; str:
        if info.context and info.context.get(&#39;mask_sensitive&#39;):
            return f&#34;{value[:4]}****{value[-2:]}&#34;
        return value

user = User(name=&#34;John&#34;, email=&#34;john@example.com&#34;, phone=&#34;0912345678&#34;)

# 一般輸出
print(user.model_dump())
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;john@example.com&#39;, &#39;phone&#39;: &#39;0912345678&#39;}

# 遮罩敏感資料
print(user.model_dump(context={&#39;mask_sensitive&#39;: True}))
# {&#39;name&#39;: &#39;John&#39;, &#39;email&#39;: &#39;jo***@example.com&#39;, &#39;phone&#39;: &#39;0912****78&#39;}
```

### 計算欄位（computed fields）

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

class OrderItem(BaseModel):
    name: str
    price: float
    quantity: int

    @computed_field
    @property
    def subtotal(self) -&gt; float:
        return self.price * self.quantity

class Order(BaseModel):
    items: List[OrderItem]
    shipping_fee: float = 0

    @computed_field
    @property
    def total(self) -&gt; float:
        items_total = sum(item.subtotal for item in self.items)
        return items_total &#43; self.shipping_fee

order = Order(
    items=[
        OrderItem(name=&#34;iPhone&#34;, price=999, quantity=1),
        OrderItem(name=&#34;Case&#34;, price=29, quantity=2),
    ],
    shipping_fee=10
)

print(order.model_dump())
# {
#     &#39;items&#39;: [
#         {&#39;name&#39;: &#39;iPhone&#39;, &#39;price&#39;: 999.0, &#39;quantity&#39;: 1, &#39;subtotal&#39;: 999.0},
#         {&#39;name&#39;: &#39;Case&#39;, &#39;price&#39;: 29.0, &#39;quantity&#39;: 2, &#39;subtotal&#39;: 58.0}
#     ],
#     &#39;shipping_fee&#39;: 10.0,
#     &#39;total&#39;: 1067.0
# }
```

---

## 🏷️ 欄位別名

### 使用 alias（序列化和反序列化都用）

```python
from pydantic import BaseModel, Field

class User(BaseModel):
    user_name: str = Field(alias=&#34;userName&#34;)
    email_address: str = Field(alias=&#34;emailAddress&#34;)

# 反序列化時使用別名
data = {&#34;userName&#34;: &#34;John&#34;, &#34;emailAddress&#34;: &#34;john@example.com&#34;}
user = User.model_validate(data)

# 序列化時也使用別名
print(user.model_dump(by_alias=True))
# {&#39;userName&#39;: &#39;John&#39;, &#39;emailAddress&#39;: &#39;john@example.com&#39;}

# 不使用別名
print(user.model_dump())
# {&#39;user_name&#39;: &#39;John&#39;, &#39;email_address&#39;: &#39;john@example.com&#39;}
```

### 使用 serialization_alias

```python
from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(serialization_alias=&#34;userName&#34;)
    email: str = Field(serialization_alias=&#34;userEmail&#34;)

user = User(name=&#34;John&#34;, email=&#34;john@example.com&#34;)

# by_alias=True 時使用 serialization_alias
print(user.model_dump(by_alias=True))
# {&#39;userName&#39;: &#39;John&#39;, &#39;userEmail&#39;: &#39;john@example.com&#39;}
```

### 使用 validation_alias

```python
from pydantic import BaseModel, Field, AliasChoices

class User(BaseModel):
    # 接受多種輸入名稱
    name: str = Field(validation_alias=AliasChoices(&#34;name&#34;, &#34;userName&#34;, &#34;user_name&#34;))

# 以下都可以
User.model_validate({&#34;name&#34;: &#34;John&#34;})
User.model_validate({&#34;userName&#34;: &#34;John&#34;})
User.model_validate({&#34;user_name&#34;: &#34;John&#34;})
```

---

## 📝 實戰範例：API 回應格式化

```python
from pydantic import BaseModel, Field, computed_field, field_serializer
from typing import List, Optional
from datetime import datetime
from decimal import Decimal
from enum import Enum

class OrderStatus(str, Enum):
    PENDING = &#34;pending&#34;
    PAID = &#34;paid&#34;
    SHIPPED = &#34;shipped&#34;
    DELIVERED = &#34;delivered&#34;

class ProductResponse(BaseModel):
    id: int
    name: str
    price: Decimal
    image_url: Optional[str] = None

    @field_serializer(&#39;price&#39;)
    def serialize_price(self, value: Decimal) -&gt; str:
        return f&#34;NT${value:,.0f}&#34;

class OrderItemResponse(BaseModel):
    product: ProductResponse
    quantity: int
    unit_price: Decimal

    @computed_field
    @property
    def subtotal(self) -&gt; Decimal:
        return self.unit_price * self.quantity

    @field_serializer(&#39;unit_price&#39;, &#39;subtotal&#39;)
    def serialize_money(self, value: Decimal) -&gt; str:
        return f&#34;NT${value:,.0f}&#34;

class OrderResponse(BaseModel):
    id: int
    order_number: str = Field(serialization_alias=&#34;orderNumber&#34;)
    status: OrderStatus
    items: List[OrderItemResponse]
    shipping_fee: Decimal = Field(serialization_alias=&#34;shippingFee&#34;)
    discount: Decimal = Decimal(&#34;0&#34;)
    created_at: datetime = Field(serialization_alias=&#34;createdAt&#34;)
    paid_at: Optional[datetime] = Field(None, serialization_alias=&#34;paidAt&#34;)

    @computed_field
    @property
    def subtotal(self) -&gt; Decimal:
        return sum(
            item.unit_price * item.quantity
            for item in self.items
        )

    @computed_field
    @property
    def total(self) -&gt; Decimal:
        return self.subtotal &#43; self.shipping_fee - self.discount

    @field_serializer(&#39;shipping_fee&#39;, &#39;discount&#39;, &#39;subtotal&#39;, &#39;total&#39;)
    def serialize_money(self, value: Decimal) -&gt; str:
        return f&#34;NT${value:,.0f}&#34;

    @field_serializer(&#39;created_at&#39;, &#39;paid_at&#39;)
    def serialize_datetime(self, value: Optional[datetime]) -&gt; Optional[str]:
        if value is None:
            return None
        return value.strftime(&#34;%Y-%m-%d %H:%M:%S&#34;)

    @field_serializer(&#39;status&#39;)
    def serialize_status(self, value: OrderStatus) -&gt; dict:
        status_display = {
            OrderStatus.PENDING: &#34;待付款&#34;,
            OrderStatus.PAID: &#34;已付款&#34;,
            OrderStatus.SHIPPED: &#34;已出貨&#34;,
            OrderStatus.DELIVERED: &#34;已送達&#34;,
        }
        return {
            &#34;code&#34;: value.value,
            &#34;display&#34;: status_display.get(value, value.value)
        }

# 使用範例
order = OrderResponse(
    id=1,
    order_number=&#34;ORD-20251217-001&#34;,
    status=OrderStatus.PAID,
    items=[
        OrderItemResponse(
            product=ProductResponse(id=1, name=&#34;iPhone 15&#34;, price=Decimal(&#34;35900&#34;)),
            quantity=1,
            unit_price=Decimal(&#34;35900&#34;)
        )
    ],
    shipping_fee=Decimal(&#34;100&#34;),
    discount=Decimal(&#34;500&#34;),
    created_at=datetime(2025, 12, 17, 10, 30, 0),
    paid_at=datetime(2025, 12, 17, 10, 35, 0)
)

import json
print(json.dumps(order.model_dump(by_alias=True), indent=2, ensure_ascii=False))
```

**輸出：**

```json
{
  &#34;id&#34;: 1,
  &#34;orderNumber&#34;: &#34;ORD-20251217-001&#34;,
  &#34;status&#34;: {
    &#34;code&#34;: &#34;paid&#34;,
    &#34;display&#34;: &#34;已付款&#34;
  },
  &#34;items&#34;: [
    {
      &#34;product&#34;: {
        &#34;id&#34;: 1,
        &#34;name&#34;: &#34;iPhone 15&#34;,
        &#34;price&#34;: &#34;NT$35,900&#34;,
        &#34;image_url&#34;: null
      },
      &#34;quantity&#34;: 1,
      &#34;unit_price&#34;: &#34;NT$35,900&#34;,
      &#34;subtotal&#34;: &#34;NT$35,900&#34;
    }
  ],
  &#34;shippingFee&#34;: &#34;NT$100&#34;,
  &#34;discount&#34;: &#34;NT$0&#34;,
  &#34;createdAt&#34;: &#34;2025-12-17 10:30:00&#34;,
  &#34;paidAt&#34;: &#34;2025-12-17 10:35:00&#34;,
  &#34;subtotal&#34;: &#34;NT$35,900&#34;,
  &#34;total&#34;: &#34;NT$35,500&#34;
}
```

---

## ✅ 重點總結

### 序列化方法

| 方法 | 輸出 | 用途 |
|------|------|------|
| `model_dump()` | dict | 轉成字典 |
| `model_dump_json()` | str | 轉成 JSON 字串 |
| `model_dump(by_alias=True)` | dict | 使用別名 |
| `model_dump(exclude_unset=True)` | dict | 排除未設定的欄位 |

### 反序列化方法

| 方法 | 輸入 | 用途 |
|------|------|------|
| `Model(**data)` | dict | 從字典建立 |
| `Model.model_validate(data)` | dict | 從字典建立（推薦）|
| `Model.model_validate_json(s)` | str | 從 JSON 建立 |

### 自訂序列化

```python
# 欄位序列化器
@field_serializer(&#39;price&#39;)
def serialize_price(self, value):
    return f&#34;${value:.2f}&#34;

# 計算欄位
@computed_field
@property
def total(self) -&gt; float:
    return self.price * self.quantity
```

---

## 🎤 面試這樣答

### Q: model_dump(exclude_unset=True) 和 exclude_defaults=True 有什麼區別？

**答案：**

&gt; - `exclude_unset=True`：排除**沒有明確傳入**的欄位，即使有預設值
&gt; - `exclude_defaults=True`：排除**值等於預設值**的欄位
&gt;
&gt; ```python
&gt; class User(BaseModel):
&gt;     name: str
&gt;     age: int = 0
&gt;
&gt; user = User(name=&#34;John&#34;, age=0)
&gt;
&gt; user.model_dump(exclude_unset=True)
&gt; # {&#39;name&#39;: &#39;John&#39;, &#39;age&#39;: 0}  # age 有明確傳入
&gt;
&gt; user.model_dump(exclude_defaults=True)
&gt; # {&#39;name&#39;: &#39;John&#39;}  # age 值等於預設值，被排除
&gt; ```

---

**上一篇：** [02-4. 巢狀模型與繼承](./02-4)
**下一篇：** [02-6. Settings 管理與環境變數](./02-6)

---

最後更新：2025-12-17


---

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

