The go-to resource for upgrading Python, Django, Flask, and your dependencies.

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.

Scenariov1 Inputv2 InputFastAPI Impact
API POSTdictdictSafe

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