troubleshooting2023-07-01·9·347/348

pydantic 모델 검증 에러 해결

A practical guide to fixing Pydantic validation errors — from basic field validation to complex custom validators, nested models, and migration between Pydantic v1 and v2.

pydantic 모델 검증 에러 해결

Introduction

Pydantic has become the de facto data validation library for Python, powering FastAPI's request/response validation and providing runtime type safety that Python's type hints cannot. However, Pydantic's error messages can be cryptic, especially when dealing with complex nested models, custom validators, or the breaking changes between v1 and v2.

I rebuilt a trading API's validation layer using Pydantic v2 in Lisbon and discovered that many of the patterns that worked in v1 had changed subtly. The migration introduced validation errors that were difficult to trace back to their source.

Environment

Python 3.12.3
Pydantic 2.7.0
FastAPI 0.111.0

Problem

Error 1: Basic field validation error

from pydantic import BaseModel, Field
from decimal import Decimal

class Order(BaseModel):
    product_id: int
    quantity: int = Field(ge=1)
    price: Decimal = Field(max_digits=10, decimal_places=2)

order = Order(product_id=1, quantity=-5, price="10.00")
ValidationError: 1 validation error for Order
quantity
  Input should be greater than or equal to 1 [type=greater_than_equal, 
  input_value=-5, input_type=int]

Error 2: Nested model validation failure

from pydantic import BaseModel
from typing import List

class Item(BaseModel):
    name: str
    price: float

class Order(BaseModel):
    items: List[Item]
    total: float

# This fails silently in some cases
order = Order(
    items=[
        {"name": "Widget", "price": "not_a_number"},  # Error!
    ],
    total=10.0
)
ValidationError: 1 validation error for Order
items.0.price
  Input should be a valid number, unable to parse string as a number 
  [type=parsing, input_value='not_a_number', input_type=str]

Error 3: Pydantic v1 to v2 migration errors

# Pydantic v1 (OLD)
from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    age: int

    @validator('age')
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

# Pydantic v2 (NEW) — this fails!
from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    age: int

    @field_validator('age')
    @classmethod
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

Error 4: Type coercion errors

from pydantic import BaseModel, ConfigDict

class StrictModel(BaseModel):
    model_config = ConfigDict(strict=True)
    
    name: str
    age: int

# This fails in strict mode
user = StrictModel(name="Alice", age="25")  # Error: age is str, not int
ValidationError: 1 validation error for StrictModel
age
  Input should be a valid integer [type=int_type, input_value='25', input_type=str]

Analysis

Pydantic validation errors occur when input data does not match the model's type annotations and constraints.

Common causes:

  1. Type mismatch: Input type does not match the expected type (string where int expected).
  2. Constraint violation: Value is outside the allowed range (negative where positive required).
  3. Missing required fields: Required fields are not provided.
  4. Extra fields: Fields not defined in the model are provided (depending on configuration).
  5. Nested model errors: Errors in nested models bubble up with path information.

Pydantic v2 changes:

  • validatorfield_validator
  • root_validatormodel_validator
  • Config class → model_config = ConfigDict()
  • schema_extrajson_schema_extra
  • __fields__model_fields

Solution

Fix 1: Handle validation errors gracefully

from pydantic import BaseModel, ValidationError
from typing import Optional

class User(BaseModel):
    name: str
    age: int
    email: Optional[str] = None

try:
    user = User(name="Alice", age=-5)
except ValidationError as e:
    print(f"Validation failed: {e}")
    # Access individual errors
    for error in e.errors():
        print(f"Field: {error['loc']}, Error: {error['msg']}")
    # Get error count
    print(f"Total errors: {len(e.errors())}")

Fix 2: Custom validators with Pydantic v2 syntax

from pydantic import BaseModel, field_validator, model_validator

class User(BaseModel):
    name: str
    age: int
    password: str
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v):
        if v < 0 or v > 150:
            raise ValueError(f'Age must be between 0 and 150, got {v}')
        return v
    
    @field_validator('name')
    @classmethod
    def validate_name(cls, v):
        if len(v.strip()) < 2:
            raise ValueError('Name must be at least 2 characters')
        return v.strip()
    
    @model_validator(mode='after')
    def validate_model(self):
        if self.age < 18 and self.password is None:
            raise ValueError('Minors must provide a password')
        return self

Fix 3: Use TypeAdapter for dynamic validation

from pydantic import TypeAdapter, ValidationError

# Validate without creating a model class
adapter = TypeAdapter(int)
try:
    result = adapter.validate_python("not_an_int")
except ValidationError as e:
    print(f"Validation failed: {e}")

# Validate complex types
from typing import List
list_adapter = TypeAdapter(List[int])
result = list_adapter.validate_python([1, 2, 3])  # Works

Fix 4: Configure model behavior

from pydantic import BaseModel, ConfigDict

class FlexibleModel(BaseModel):
    model_config = ConfigDict(
        strict=False,           # Allow type coercion
        extra='ignore',         # Ignore extra fields
        validate_assignment=True,  # Validate on attribute set
    )
    
    name: str
    age: int

# Extra fields are ignored
user = FlexibleModel(name="Alice", age=25, extra_field="ignored")

Fix 5: Custom error messages

from pydantic import BaseModel, Field, ValidationError

class Order(BaseModel):
    product_id: int = Field(
        gt=0,
        description="Product identifier",
        json_schema_extra={"examples": [1, 2, 3]}
    )
    quantity: int = Field(
        gt=0,
        le=10000,
        description="Number of items"
    )
    price: float = Field(
        gt=0,
        description="Price per unit"
    )

# Custom error messages in v2
from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    
    @field_validator('name')
    @classmethod
    def validate_name(cls, v):
        if len(v) < 2:
            raise ValueError(
                'Name must be at least 2 characters long. '
                f'Got {len(v)} characters.'
            )
        return v

Fix 6: Pydantic v1 to v2 migration checklist

# BEFORE (Pydantic v1)
from pydantic import BaseModel, validator, root_validator

class OldUser(BaseModel):
    name: str
    age: int

    @validator('age')
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

# AFTER (Pydantic v2)
from pydantic import BaseModel, field_validator, model_validator, ConfigDict

class NewUser(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    
    name: str
    age: int

    @field_validator('age')
    @classmethod
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v
    
    @model_validator(mode='after')
    def validate_model(self):
        if self.name and not self.name.strip():
            raise ValueError('Name cannot be empty')
        return self

Lessons Learned

  • Always wrap model creation in try/except ValidationError to handle invalid input gracefully.
  • Use field_validator and model_validator for custom validation logic in Pydantic v2.
  • Use ConfigDict instead of inner Config class for Pydantic v2 models.
  • Test validation separately from the rest of your application to catch edge cases early.
  • When migrating from v1 to v2, use the Pydantic migration guide and run both versions side by side during the transition.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.