Pydantic v2 정리 (@model_validator, @field_validator, @model_serializer, @field_serializer)

류지수·2025년 6월 10일

Pytdantic v2란?

Python 타입 기반 데이터 검증 및 직렬화 라이브러리
v2에서는 validatorserializer를 분리해 더 명확한 역할 제공

모델 선언

pydatic의 핵심은 BaseModel 상속하는 것

from pydantic import BaseModel, Field

class User(BaseModel):
    id: int
    name: str
    password: str = Field(..., min_length=4)
  • field로 validation 규칙 설명 가능
  • 타입 힌팅으로 기본적인 타입 검증


데이터 검증 (validator)

데이터 들어올 때 값을 검사하고 조정할 수 있음

모델 전체 검증: @model_validator

mode동작 시점첫 번째 인자
before인스턴스 생성 전cls
after인스턴스 생성 후self
wrap생성 과정 감싸기validator 콜백

Before validators

  • 모델이 인스턴스화되기 전에 실행됨. After Validatores보다 유연하지만, 이론적으로 임의의 개체가 될 수 있는 raw 데이터를 다뤄야함.
from typing import Any

from pydantic import BaseModel, model_validator


class UserModel(BaseModel):
    username: str

    @model_validator(mode='before')
    @classmethod
    def check_card_number_not_present(cls, data: Any) -> Any:  
        if isinstance(data, dict):  
            if 'card_number' in data:
                raise ValueError("'card_number' should not be included")
        return data

After validators

  • 전체 모델이 검증된 후에 실행. 따라서 인스턴스 메서드로 정의되며 초기화 후 후크로 볼 수 있음. (중요: 검증된 인스터스는 반환되어야 한다.)
from typing_extensions import Self

from pydantic import BaseModel, model_validator


class UserModel(BaseModel):
    username: str
    password: str
    password_repeat: str

    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        if self.password != self.password_repeat:
            raise ValueError('Passwords do not match')
        return self

Wrap validators

  • 가장 유연한 검증기. Pydnatic 및 기타 validator가 입력 데이터를 처리하기 전이나 후에 코드를 실행할 수 있으며, 데이터를 조기에 반환하거나 오류를 발생시켜 검증을 즉시 종료할 수 있다.
import logging
from typing import Any

from typing_extensions import Self

from pydantic import BaseModel, ModelWrapValidatorHandler, ValidationError, model_validator


class UserModel(BaseModel):
    username: str

    @model_validator(mode='wrap')
    @classmethod
    def log_failed_validation(cls, data: Any, handler: ModelWrapValidatorHandler[Self]) -> Self:
        try:
            return handler(data)
        except ValidationError:
            logging.error('Model %s failed to validate with data %s', cls, data)
            raise


개별 필드 검증: @field_validator

from pydantic import BaseModel, ValidationInfo, field_validator


class UserModel(BaseModel):
    password: str
    password_repeat: str
    username: str

    @field_validator('password_repeat', mode='after')
    @classmethod
    def check_passwords_match(cls, value: str, info: ValidationInfo) -> str:
        if value != info.data['password']:
            raise ValueError('Passwords do not match')
        return value

*상세 설명

  • check_passwrods_match는 @field_validator('password_repeat', mode='after')
  • 이는 password_repeat 필드의 유효성 검사가 완료된 후 (mode='after') check_passwords_match함수가 실행된다는 의미
  • 이 함수 안에서는 value (현재 password_repeat의 값)와 into.data['password'] (이미 유효성 검사가 완료된 password 의 값)를 비교하여 두 비밀번호가 일치하는지 확인한다.

주의해야할 점

  • 유효성 검사는 필드가 정의된 순서대로 수행되므로, 아직 유효성 검사가 완료되지 않은 필드에 접근하지 않은 필드에 접근하지 않도록 해야한다.
  • 위의 코드를 예시에서 username의 유효성이 검사된 값은 아직 사용할 수 없다. usernamepassword_repeat 뒤에 정의되어 있기 때문

*상세설명

  1. Pydantic은 BaseModel을 상속받아 정의된 클래스에서 필드를 선언한 순서대로 유효성 검사를 진행
  2. 위의 UserModel 클래스를 보면 필드가 다음과 같은 순서로 정의
    1) password
    2) password_repaeat
    3) username
  • 따라서 Pydantic은 데이터를 받을 때 password의 유효성을 먼저 검사하고, 그 다음 password_repeat, 마지막으로 username 순으로 검사 수행


직렬화 (serializer)

데이터 나갈 때 값을 변환가능
→ API 응답 포맷 가공용

모델 전체 직렬화: @model_serializer

from pydantic import model_serializer

class User(BaseModel):
    id: int
    password: str

    @model_serializer
    def serialize(self):
        return {
            "id": self.id
        }
        
user = User(id=1, password='secret')
print(user.model_dump())
# {'id': 1}

@model_serializer 의 역할

  • Pydantic 모델 전체를 어떻게 JSON(이나 다른 형태로)으로 변환(직렬화) 할 것인지 정의할 때 사용

  • 일반적으로 Pydantic 모델 인스턴스를 .model_dump().model_json()으로 변환하면, 모델에 정의된 모든 필드가 기본적으로 포함됨

  • 하지만 @model_serializer를 사용하면, 이 serialize 메서드가 실행되어 반환하는 딕셔너리가 해당 모델 인스턴스의 직렬화된 결과물이 된다.

  • 예를들어 @model_serializer 없이 할 경우는 다음과 같다.

from pydantic import model_serializer

class User(BaseModel):
    id: int
    password: str

    def serialize(self):
        return {
            "id": self.id
        }
        
user = User(id=1, password="secret")
print(user.model_dump())
# 출력: {'id': 1, 'password': 'secret'} # <--- password가 포함!

# serialize 메서드를 사용하려면 직접 호출해야 한다.
print(user.serialize()) 
# 출력: {'id': 1}
  • 주요 사용 목적: 보안상 민감한 정보(예: 비밀번호)를 외부에 노출하지 않거나, 특정 필드만 필요한 경우 모델 전체의 직렬화 방식을 커스터마이징할 때 사용

개별 필드 직렬화: @field_serializer

from pydantic import field_serializer

class Product(BaseModel):
    price: int

    @field_serializer("price")
    def format_price(self, v):
        return f"{v:,}원"

@field_serializer의 역할:

  • Pydantic 모델의 특정 필드가 어떻게 직렬화될 것인지 정의할 때 사용
  • 모델 전체의 직렬화 방식을 바꾸는 @model_serializer와 달리, @field_serializer는 개별 필드의 출력 형식을 제어함

*상세 설명

  • Product 모델은 price라는 정수(int) 필드를 가지고 있음
  • @field_serializer('price') 데코레이터가 적용된 format_price 메서드는 price 필드의 직렬화 시 실행된다.
  • format_price 메서드는 입력값 v (여기서는 price의 값)를 받아서 f"{v:,}원 형태로 변환한다.


전체 플로우

[데이터 들어옴]
  ↓
@model_validator(before) 로 값 세팅 및 초기 정리
  ↓
@field_validator (before/after) 로 개별 필드 값 검증
  ↓
@model_validator(after) 로 전체 값 확인
  ↓
ValidationError 발생 시 예외 처리
  ↓
model_dump()/model_dump_json() 으로 직렬화
  ↓
@field_serializer/@model_serializer 로 응답 포맷 가공
  ↓
API 응답 or 저장
profile
끄적끄적

0개의 댓글