跳到主要內容
程式語言

Pydantic 入門:用型別註解打造 Python 的執行期資料驗證

從 Java/JavaScript 開發者的視角理解 Pydantic,涵蓋核心概念、常用功能,以及它為何成為 FastAPI 與 LLM structured output 的基石。

如果你寫過 Java 或 TypeScript,你一定遇過這個問題:型別系統只在編譯時期保護你,runtime 進來的資料(API request、JSON 檔、資料庫查詢結果)依然可能不符預期

Pydantic 解決的就是這件事——它讓 Python 的 type hints 從「給 IDE 看的註解」變成「執行期強制驗證的合約」。

一句話定位

Pydantic = 用 Python type hints 宣告資料模型,在物件建立時自動驗證、轉型,失敗就丟出結構化的錯誤。

對照你可能熟悉的工具:

生態系對應工具角色
JavaBean Validation + Jackson欄位驗證 + JSON 序列化
TypeScriptZodruntime schema 驗證
PythonPydantic兩者合體

安裝

pip install pydantic

本文以 Pydantic v2 為準(API 與 v1 有不少差異,v2 核心以 Rust 重寫,效能大幅提升)。

基本用法:BaseModel

from pydantic import BaseModel, Field

class User(BaseModel):
    name: str
    age: int = Field(ge=0, le=150)   # 數值範圍限制
    email: str | None = None          # 可選欄位,預設 None

u = User(name="Albert", age="35")    # 注意 age 傳入的是字串
print(u.age)        # 35 → 已自動轉成 int
print(type(u.age))  # <class 'int'>

兩個關鍵行為:

  1. 自動轉型(coercion)"35" 會被轉成 35。這對處理 query string、環境變數這類「天生都是字串」的資料來源特別實用。
  2. 建構時驗證:不合法的資料根本建立不出物件,問題資料不會活著進到系統深處。

驗證失敗時會丟出 ValidationError,錯誤訊息精確到欄位:

from pydantic import ValidationError

try:
    User(name="Bob", age=-5)
except ValidationError as e:
    print(e)
    # 1 validation error for User
    # age
    #   Input should be greater than or equal to 0 [type=greater_than_equal, ...]

對 Java 開發者來說,這等於 @Valid + @Min(0) 的效果,但不需要任何 annotation processor 或框架支援,純 Python 就能跑。

JSON 序列化與反序列化

這部分扮演 Jackson 的角色:

# 物件 → JSON 字串
u.model_dump_json()
# '{"name":"Albert","age":35,"email":null}'

# 物件 → dict
u.model_dump()
# {'name': 'Albert', 'age': 35, 'email': None}

# JSON 字串 → 驗證過的物件
raw = '{"name": "Carol", "age": 28}'
user = User.model_validate_json(raw)

# dict → 物件
data = {"name": "Dave", "age": 41}
user = User.model_validate(data)

注意 v2 的命名慣例:方法都以 model_ 開頭(v1 是 .json().parse_raw(),升級時要留意)。

巢狀模型

模型可以任意組合,驗證會遞迴進行:

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

class Order(BaseModel):
    order_id: int
    user: User
    shipping: Address
    items: list[str]

order = Order.model_validate({
    "order_id": 1001,
    "user": {"name": "Albert", "age": 35},
    "shipping": {"city": "Tainan", "zip_code": "700"},
    "items": ["keyboard", "mouse"],
})

傳入巢狀 dict,Pydantic 會自動建構出對應的子模型物件——這在解析複雜 API response 時省下大量手動 mapping 的程式碼。

自訂驗證邏輯

內建約束不夠用時,可以掛自訂 validator:

from pydantic import BaseModel, field_validator, model_validator

class Booking(BaseModel):
    start: int
    end: int

    @field_validator("start", "end")
    @classmethod
    def must_be_positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("必須為正數")
        return v

    @model_validator(mode="after")
    def end_after_start(self):
        if self.end <= self.start:
            raise ValueError("end 必須大於 start")
        return self
  • field_validator:驗證單一欄位,類似 Java 的自訂 ConstraintValidator
  • model_validator:跨欄位驗證,可以存取整個物件。

常用進階功能速覽

預設值工廠

from pydantic import Field
from datetime import datetime

class Log(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)
    tags: list[str] = Field(default_factory=list)  # 避免 mutable default 陷阱

嚴格模式

不想要自動轉型(例如 "35"35)時:

class StrictUser(BaseModel):
    model_config = {"strict": True}
    age: int  # 傳 "35" 會直接報錯

環境變數設定管理

pydantic-settings 套件可以把 .env / 環境變數直接載入成型別安全的設定物件:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    debug: bool = False

settings = Settings()  # 自動讀取環境變數 DATABASE_URL、DEBUG

這比手動 os.getenv() 再轉型乾淨得多,也是 12-factor app 設定管理的常見做法。

為什麼 Pydantic 在 LLM 時代爆紅

Pydantic 模型可以直接輸出 JSON Schema

User.model_json_schema()
# {
#   "title": "User",
#   "type": "object",
#   "properties": {
#     "name": {"type": "string"},
#     "age": {"type": "integer", "minimum": 0, "maximum": 150},
#     ...
#   },
#   "required": ["name", "age"]
# }

這一個能力串起了整個現代 Python 生態:

  • FastAPI:request/response 模型直接用 Pydantic 宣告,自動產生驗證邏輯 + OpenAPI 文件。
  • OpenAI structured outputs / function calling:工具參數的 schema 來自 Pydantic 模型。
  • vLLM guided_json / Outlines:約束本地 LLM 只能輸出符合 schema 的 JSON,再用同一個模型 model_validate_json() 解析回來——定義一次,驗證、文件、LLM 輸出約束三件事同時搞定

一個典型的 LLM 結構化抽取流程:

class Invoice(BaseModel):
    vendor: str
    amount: float
    issue_date: str

schema = Invoice.model_json_schema()
# 把 schema 餵給 LLM 的 guided_json / response_format

llm_output = call_llm(prompt, json_schema=schema)
invoice = Invoice.model_validate_json(llm_output)  # 失敗就丟 ValidationError,可重試

LLM 輸出不可靠,但有了 Pydantic 這道閘門,壞輸出會在邊界就被攔下,而不是污染下游邏輯。

與 dataclass 的差異

Python 內建的 @dataclass 也能宣告欄位,但不做任何驗證

dataclassPydantic BaseModel
型別宣告
執行期驗證
自動轉型
JSON 序列化需自己處理內建
JSON Schema內建

經驗法則:純內部資料容器用 dataclass,任何跨越信任邊界的資料(API、檔案、LLM 輸出、使用者輸入)用 Pydantic

小結

Pydantic 的價值可以濃縮成一句話:讓「資料的形狀」成為程式碼的一部分,而且是會被強制執行的那種

  • 對 Java 開發者:像 Bean Validation + Jackson,但零框架負擔。
  • 對 TypeScript 開發者:就是 Python 的 Zod。
  • 對所有人:它是 FastAPI 與 LLM structured output 的共同地基,投資報酬率極高。

官方文件:https://docs.pydantic.dev/