본문으로 건너뛰기
Advertisement

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의 검증·직렬화·문서화를 하나로 통합하는 핵심 레이어입니다.

Advertisement