Pydantic V2 is a major release with significant performance improvements and new features, but it includes breaking changes. This guide will help you migrate your code successfully.
Quick Start
Use the migration tool
pip install bump-pydantic
bump-pydantic /path/to/your/code
Test and fix remaining issues
Review the changes and address any issues the tool couldn’t fix automatically.
The bump-pydantic tool automatically updates most V1 code to V2:
# Install the tool
pip install bump-pydantic
# Run on your project
cd /path/to/your/project
bump-pydantic my_package
Continue Using V1 Features
If you need more time to migrate, Pydantic V2 includes V1 compatibility:
# Import from pydantic.v1 instead
from pydantic.v1 import BaseModel
class User ( BaseModel ):
name: str
age: int
Python 3.14 Compatibility: Pydantic V1 is not compatible with Python 3.14 and greater. You must migrate to V2 to use newer Python versions.
Major Breaking Changes
Method Renames
Many BaseModel methods have been renamed with a model_ prefix:
Pydantic V1 Pydantic V2 dict()model_dump()json()model_dump_json()parse_obj()model_validate()parse_raw()model_validate_json()parse_file()❌ Removed (load data manually) construct()model_construct()copy()model_copy()schema()model_json_schema()update_forward_refs()model_rebuild()
Migration Example:
from pydantic import BaseModel
class User ( BaseModel ):
name: str
age: int
user = User( name = 'John' , age = 30 )
# V1 methods
data = user.dict()
json_str = user.json()
user2 = User.parse_obj({ 'name' : 'Jane' , 'age' : 25 })
Configuration Changes
Configuration moved from a nested Config class to model_config dict:
from pydantic import BaseModel
class User ( BaseModel ):
name: str
email: str
class Config :
str_strip_whitespace = True
validate_assignment = True
Config Options Renamed
V1 Config V2 Config allow_mutationfrozen (inverse)allow_population_by_field_namepopulate_by_nameanystr_lowerstr_to_loweranystr_strip_whitespacestr_strip_whitespaceanystr_upperstr_to_upperorm_modefrom_attributesschema_extrajson_schema_extravalidate_allvalidate_default
Example:
# V1
class Config :
orm_mode = True
allow_mutation = False
# V2
model_config = ConfigDict(
from_attributes = True ,
frozen = True , # Note: inverse of allow_mutation
)
Validators: @validator → @field_validator
V1 Validator
V2 Field Validator
from pydantic import BaseModel, validator
class User ( BaseModel ):
name: str
age: int
@validator ( 'age' )
def age_must_be_positive ( cls , v ):
if v <= 0 :
raise ValueError ( 'must be positive' )
return v
Key differences:
Import from field_validator instead of validator
The @classmethod decorator is now required
Better type hints are recommended
Root Validators: @root_validator → @model_validator
V1 Root Validator
V2 Model Validator
from pydantic import BaseModel, root_validator
class DateRange ( BaseModel ):
start: str
end: str
@root_validator
def check_dates ( cls , values ):
if values.get( 'start' ) >= values.get( 'end' ):
raise ValueError ( 'start must be before end' )
return values
Important: In V2 mode='after', the validator receives the model instance, not a dict of values.
Field Changes
Several Field parameters were removed or renamed:
V1 Parameter V2 Parameter min_itemsmin_lengthmax_itemsmax_lengthregexpatternallow_mutationfrozenconst❌ Use Literal instead
Example:
# V1
from pydantic import BaseModel, Field
class Product ( BaseModel ):
tags: list[ str ] = Field( min_items = 1 , max_items = 5 )
sku: str = Field( regex = r ' ^ [ A-Z ] {3} \d {3} $ ' )
# V2
from pydantic import BaseModel, Field
class Product ( BaseModel ):
tags: list[ str ] = Field( min_length = 1 , max_length = 5 )
sku: str = Field( pattern = r ' ^ [ A-Z ] {3} \d {3} $ ' )
GenericModel Removed
No more GenericModel - use BaseModel directly with Generic:
from typing import Generic, TypeVar
from pydantic.generics import GenericModel
T = TypeVar( 'T' )
class Response ( GenericModel , Generic[T]):
data: T
status: int
Root Models
Custom root types now use RootModel:
V1 Root Model
V2 Root Model
from pydantic import BaseModel
class MyList ( BaseModel ):
__root__: list[ int ]
ml = MyList( __root__ = [ 1 , 2 , 3 ])
print (ml.__root__)
# [1, 2, 3]
Optional Fields Behavior
Breaking Change: Optional[T] fields are now required by default:
from typing import Optional
from pydantic import BaseModel
class User ( BaseModel ):
name: str
nickname: Optional[ str ] # Required in V2! Can be None.
age: Optional[ int ] = None # Optional with default
# V2 behavior
User( name = 'John' , nickname = None ) # Valid
User( name = 'John' ) # ValidationError: nickname is required
User( name = 'John' , nickname = None , age = 25 ) # Valid
V1 vs V2: In V1, Optional[str] meant “optional field with default None”. In V2, it means “required field that can be None”.
Type Coercion Changes
Float to int conversion is stricter:
from pydantic import BaseModel
class Model ( BaseModel ):
count: int
# V1: Accepted any float
Model( count = 10.5 ) # Worked in V1, count=10
# V2: Only accepts floats with zero decimal part
Model( count = 10.0 ) # Works, count=10
Model( count = 10.5 ) # ValidationError in V2
Union Behavior Changes
V2 uses “smart” union validation by default:
from typing import Union
from pydantic import BaseModel
class Model ( BaseModel ):
value: Union[ int , str ]
# V1: Left-to-right, first match wins
m1 = Model( value = '123' ) # V1: value=123 (int)
# V2: Smart mode preserves input type when possible
m2 = Model( value = '123' ) # V2: value='123' (str)
To get V1 behavior, use union_mode='left_to_right':
from typing import Union
from pydantic import BaseModel, Field
class Model ( BaseModel ):
value: Union[ int , str ] = Field( union_mode = 'left_to_right' )
m = Model( value = '123' ) # value=123 (int)
Removed Features
Constrained Types
V1 Constrained
V2 Annotated
from pydantic import ConstrainedInt, BaseModel
class MyInt ( ConstrainedInt ):
ge = 0
le = 100
class Model ( BaseModel ):
score: MyInt
JSON Encoders
json_encoders config is deprecated. Use @field_serializer instead:
V1 json_encoders
V2 field_serializer
from datetime import datetime
from pydantic import BaseModel
class Event ( BaseModel ):
timestamp: datetime
class Config :
json_encoders = {
datetime: lambda v : v.strftime( '%Y-%m- %d ' )
}
BaseSettings Moved
BaseSettings is now in a separate package:
pip install pydantic-settings
# V1
from pydantic import BaseSettings
# V2
from pydantic_settings import BaseSettings
TypeAdapter for Non-Models
Replaces parse_obj_as:
V1 parse_obj_as
V2 TypeAdapter
from pydantic import parse_obj_as
result = parse_obj_as(list[ int ], [ '1' , '2' , '3' ])
print (result)
# [1, 2, 3]
Pydantic V2 is significantly faster than V1:
Rust core: Validation is 5-50x faster depending on use case
JSON parsing: Built-in JSON parsing is much faster
Serialization: 2-10x faster serialization
Schema generation: Faster JSON schema generation
Migration Checklist
Common Migration Issues
Issue: Optional fields now required
Problem: class User ( BaseModel ):
nickname: Optional[ str ] # Was optional in V1, required in V2
Solution: class User ( BaseModel ):
nickname: Optional[ str ] = None # Explicitly set default
Issue: ValidationError format changed
Issue: Model equality behavior changed
Problem: Models no longer equal to dicts# V1: This worked
model = User( name = 'John' , age = 30 )
assert model == { 'name' : 'John' , 'age' : 30 } # True in V1
# V2: Must compare to dict explicitly
assert model.model_dump() == { 'name' : 'John' , 'age' : 30 }
Getting Help
If you encounter issues during migration:
Check the Pydantic V2 Documentation
Search GitHub Issues
Ask on GitHub Discussions
Report bugs with the bug V2 label
See Also