Pydantic v2 모델
FastAPI는 Pydantic v2를 기반으로 요청 검증, 응답 직렬화, 설정 관리를 처리합니다. Pydantic v2는 Rust 기반으로 재작성되어 v1 대비 5~50배 빠릅니다.
BaseModel 기초
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
class User(BaseModel):
id: int
name: str
email: str
age: Optional[int] = None
created_at: datetime = Field(default_factory=datetime.now)
# 모델 설정
model_config = {
"str_strip_whitespace": True, # 문자열 앞뒤 공백 제거
"validate_assignment": True, # 대입 시에도 검증
}
# 생성
user = User(id=1, name=" Alice ", email="alice@example.com")
print(user.name) # "Alice" (공백 제거됨)
print(user.model_dump()) # 딕셔너리 변환
print(user.model_dump_json()) # JSON 문자열
# 파싱
data = {"id": "2", "name": "Bob", "email": "bob@test.com"}
user2 = User.model_validate(data) # 타입 자동 변환: "2" → 2
Field — 필드 검증
from pydantic import BaseModel, Field
from typing import Annotated
class Product(BaseModel):
# 필수 필드 (기본값 없음)
name: str = Field(..., min_length=1, max_length=100, description="상품명")
# 숫자 범위
price: float = Field(..., gt=0, description="가격 (0 초과)")
stock: int = Field(0, ge=0, description="재고 (0 이상)")
discount: float = Field(0.0, ge=0.0, le=1.0, description="할인율 0~1")
# 정규식 검증
sku: str = Field(..., pattern=r"^[A-Z]{3}-\d{4}$", description="예: ABC-1234")
# 별칭 (JSON 키 이름 다르게)
product_type: str = Field(..., alias="type")
# 직렬화 시 다른 이름
internal_code: str = Field(..., serialization_alias="code")
# alias 사용 예
data = {"name": "Python 책", "price": 35000, "sku": "PYT-0001", "type": "book", "code": "B001"}
product = Product.model_validate(data)
print(product.product_type) # "book"
# alias로 직렬화
print(product.model_dump(by_alias=True))
field_validator — 커스텀 검증
from pydantic import BaseModel, Field, field_validator, model_validator
import re
class UserCreate(BaseModel):
username: str
email: str
password: str
confirm_password: str
age: int
# 단일 필드 검증
@field_validator("username")
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("사용자명은 영문·숫자만 허용됩니다")
return v.lower()
@field_validator("email")
@classmethod
def email_format(cls, v: str) -> str:
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, v):
raise ValueError("이메일 형식이 올바르지 않습니다")
return v.lower()
@field_validator("age")
@classmethod
def age_range(cls, v: int) -> int:
if not (0 <= v <= 150):
raise ValueError(f"유효하지 않은 나이: {v}")
return v
# 여러 필드 동시 검증 (model_validator)
@model_validator(mode="after")
def passwords_match(self) -> "UserCreate":
if self.password != self.confirm_password:
raise ValueError("비밀번호가 일치하지 않습니다")
return self
# 검증 실패 시 ValidationError
from pydantic import ValidationError
try:
user = UserCreate(
username="alice!!",
email="not-email",
password="secret",
confirm_password="different",
age=200,
)
except ValidationError as e:
for error in e.errors():
print(f"{error['loc']}: {error['msg']}")
중첩 모델
from pydantic import BaseModel
from typing import Optional
class Address(BaseModel):
street: str
city: str
country: str = "KR"
zip_code: Optional[str] = None
class Company(BaseModel):
name: str
address: Address
class Employee(BaseModel):
id: int
name: str
company: Company
addresses: list[Address] = []
# 중첩 구조 파싱
data = {
"id": 1,
"name": "Alice",
"company": {
"name": "Tech Corp",
"address": {
"street": "강남대로 1",
"city": "서울",
},
},
"addresses": [
{"street": "자택 주소", "city": "서울"},
],
}
emp = Employee.model_validate(data)
print(emp.company.address.city) # 서울
# 깊은 dict 변환
print(emp.model_dump())
응답 모델 패턴
from fastapi import FastAPI
from pydantic import BaseModel, Field
from datetime import datetime
app = FastAPI()
# 요청/응답 모델 분리
class UserCreate(BaseModel):
"""생성 요청 모델 (비밀번호 포함)"""
name: str
email: str
password: str
class UserUpdate(BaseModel):
"""업데이트 요청 모델 (모든 필드 선택)"""
name: str | None = None
email: str | None = None
class UserResponse(BaseModel):
"""응답 모델 (비밀번호 제외)"""
id: int
name: str
email: str
created_at: datetime
model_config = {"from_attributes": True} # ORM 객체 자동 변환
# response_model_exclude_none: None 필드 제외
class PartialResponse(BaseModel):
id: int
name: str
score: float | None = None
rank: int | None = None
@app.get(
"/users/{user_id}",
response_model=PartialResponse,
response_model_exclude_none=True, # None 필드 JSON 미포함
)
def get_user(user_id: int):
return PartialResponse(id=user_id, name="Alice", score=None, rank=None)
# 응답: {"id": 1, "name": "Alice"} (score, rank 제외)
정리
| 기능 | 방법 |
|---|---|
| 기본 검증 | Field(min_length=1, gt=0, pattern=...) |
| 커스텀 검증 | @field_validator("field") |
| 전체 모델 검증 | @model_validator(mode="after") |
| ORM 변환 | model_config = {"from_attributes": True} |
| dict 변환 | model.model_dump() |
| JSON 변환 | model.model_dump_json() |
| 파싱 | Model.model_validate(data) |
| None 제외 직렬화 | response_model_exclude_none=True |
Pydantic v2는 FastAPI의 검증·직렬화·문서화를 하나로 통합하는 핵심 레이어입니다.