Pydantic v2 Migration in FastAPI: Fixing model_validator Decorator Breaking Changes
If you’re migrating a FastAPI app to Pydantic v2—for the performance improvements and stricter typing—you might encounter a breaking change in model validators. In v1, @root_validator(pre=True) always received a plain dictionary. Now, @model_validator(mode='before') receives either a dict or a partially constructed model instance, depending on the validation context.\n\nThis change enables validating field assignments on instances when validate_assignment=True. FastAPI endpoints remain safe, as they parse JSON to dicts first. Other code paths, like tests or CLI, may break if your validator assumes a dict.
Breaking Change Details
The input type depends on how Pydantic invokes the validator.
| Scenario | v1 Input | v2 Input | FastAPI Impact |
|---|---|---|---|
| API POST | dict | dict | Safe |
Error:
TypeError: Model.model_validator.<locals>.validate() missing 1 required positional argument: 'values'
V1 → V2 Migration Fix
Consider a common v1 validator that checks the email domain across fields before individual validation:\n\nv1 version:\n```python\nfrom pydantic import root_validator, BaseModel
class User(BaseModel): name: str email: str
@root_validator(pre=True)
def check_domain(cls, values):
if 'email' in values and '@evil.com' in values['email']:
raise ValueError('Evil domain')
return values\n```\n\nThe v2 equivalent handles both input types. We check if data is a dict; if not, dump the instance to dict for validation. Return the processed dict or original instance to let validation continue.\n\n**v2 version**:
from pydantic import BaseModel, model_validator
from typing import Any, Dict
class User(BaseModel):
name: str
email: str
model_config = {'validate_assignment': True}
@model_validator(mode='before')
@classmethod
def check_domain(cls, data: Any) -> Dict[str, Any]:
if isinstance(data, dict):
values = data
else: # instance
values = data.model_dump(mode='json') # or data.dict()
if 'email' in values and '@evil.com' in values['email']:
raise ValueError('Evil domain')
return values if isinstance(data, dict) else data
FastAPI-Specific Fixes
FastAPI request bodies arrive as JSON, parsed to dicts before validation. Endpoints see dict input:\n\nEndpoint example:\n```python\nfrom fastapi import FastAPI from pydantic import BaseModel, model_validator
app = FastAPI()
class UserCreate(BaseModel): # Your model above
@app.post(‘/users/’)
def create_user(user: UserCreate):
return user\n\n\n**Breaks in utils or tests** where you build models incrementally:\npython
utils.py → CLI/tests
user = User(name=‘test’)
user.email = ‘test@evil.com’ # Triggers validator with instance\n\n\n**Tests using dict input work** (model_validate(dict)):\npython
def test_user():
user = User.model_validate({‘name’: ‘ok’, ‘email’: ‘good@example.com’})
assert user.email == ‘good@example.com’\n\n\nFor assignment tests, either disable validate_assignment or use dict input.\n\n## Alternatives: field_validator vs model_validator\n\nFor validations on single fields, `@field_validator` often suffices—and avoids the dict/instance handling entirely. It operates after type coercion on individual values, so no raw input worries.\n\nModel_validator with `mode='before'` excels at cross-field checks on raw data, like our email domain example. However, it requires the type handling we added. Choose based on needs: field_validator for simplicity and speed; model_validator for raw, multi-field logic.\n\nWe can mix both in the same model.\n\n## Verify Your Migration\n\nFirst, create `main.py` with your FastAPI app and User model:\n\n[Include brief main.py example here, but assume reader adapts]\n\nRun these commands:\n\nbash\n$ pip install “pydantic>=2,<3” fastapi uvicorn\n# Expect: Successfully installed pydantic-2.x.x … \n\n$ uvicorn main:app —reload\n# Expect: INFO: Uvicorn running on http://127.0.0.1:8000\n\n# In another terminal:\n$ curl -X POST “http://localhost:8000/users/” \\n -H “Content-Type: application/json” \\n -d ’{“name”:“test”,“email”:“test@example.com”}‘\n# Expect: {“name”:“test”,“email”:“test@example.com”}\n```\n\nPytest example (test_user.py):\n
python\nimport pytest\nfrom your_app import User\n\ndef test_valid():\n User.model_validate({'name": 'a', 'email': 'a@example.com'})\n\ndef test_evil():\n with pytest.raises(ValueError):\n User.model_validate({'name': 'a', 'email': 'a@evil.com'})\n\n\nRun pytest test_user.py—expect 2 passed, no TypeErrors.\n\n## Performance and Best Practices
The isinstance(data, dict) check adds negligible runtime cost.\n\nPrefer @field_validator for single-field logic. It avoids cross-field complexity and the input type issue.\n\nFastAPI 0.104+ supports Pydantic v2 natively. Earlier versions need pydantic-settings.
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming