FastAPI에서 Pydantic으로 복잡한 중첩 응답 모델 검증하기

문제 상황

결제 서비스 API를 FastAPI로 wrapping하는 프록시 서버를 구축하던 중, 외부 API 응답 구조가 예상과 다른 경우가 종종 발생했다. 특히 중첩된 객체의 필드가 누락되거나 타입이 다를 때 디버깅이 어려웠다.

# 기존 코드 - 타입 힌트만 있고 검증은 없음
async def get_payment_info(payment_id: str) -> dict:
    response = await external_api.get(f"/payments/{payment_id}")
    return response.json()

Pydantic 모델 정의

응답 구조를 Pydantic 모델로 명시했다. 중첩된 구조는 별도 모델로 분리했다.

from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime

class PaymentMethod(BaseModel):
    type: str
    last4: Optional[str] = None
    brand: Optional[str] = None

class PaymentDetail(BaseModel):
    id: str
    amount: int
    currency: str = Field(default="KRW")
    status: str
    method: PaymentMethod
    created_at: datetime
    metadata: Optional[dict] = {}

@app.get("/api/payments/{payment_id}", response_model=PaymentDetail)
async def get_payment_info(payment_id: str):
    response = await external_api.get(f"/payments/{payment_id}")
    # Pydantic이 자동으로 검증
    return PaymentDetail(**response.json())

얻은 것

  1. 런타임 검증: 외부 API 응답이 스키마와 다르면 즉시 ValidationError 발생
  2. 자동 문서화: FastAPI가 Pydantic 모델 기반으로 OpenAPI 스키마 생성
  3. IDE 지원: VSCode에서 자동완성과 타입 체크 정상 동작
  4. 타입 강제 변환: created_at 문자열을 datetime 객체로 자동 파싱

특히 Fielddefault, alias, gt/lt 같은 옵션으로 세밀한 제약 조건을 설정할 수 있어 유용했다.

주의사항

OptionalNone 기본값을 혼동하지 않도록 주의했다. Optional[str]None을 허용하지만, 기본값이 없으면 여전히 필수 필드다. 누락 가능한 필드는 명시적으로 = None을 붙여야 한다.

FastAPI에서 Pydantic으로 복잡한 중첩 응답 모델 검증하기