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.0Problem
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 vError 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 intValidationError: 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:
- Type mismatch: Input type does not match the expected type (string where int expected).
- Constraint violation: Value is outside the allowed range (negative where positive required).
- Missing required fields: Required fields are not provided.
- Extra fields: Fields not defined in the model are provided (depending on configuration).
- Nested model errors: Errors in nested models bubble up with path information.
Pydantic v2 changes:
validator→field_validatorroot_validator→model_validatorConfigclass →model_config = ConfigDict()schema_extra→json_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 selfFix 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]) # WorksFix 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 vFix 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 selfLessons Learned
- Always wrap model creation in try/except ValidationError to handle invalid input gracefully.
- Use
field_validatorandmodel_validatorfor custom validation logic in Pydantic v2. - Use
ConfigDictinstead of innerConfigclass 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.