Pydantic V2.10: Enhanced Validation, TypeScript Types, and Custom Serializers
Pydantic V2.10 brings improved field validation, native TypeScript type generation, and flexible custom serializers. Learn what's new and how to upgrade your data validation pipelines.
Introduction
Pydantic has become the de facto standard for data validation in Python, and with V2.10, the team continues to refine and extend its capabilities. Released in late 2024, this update introduces several developer-focused improvements that make validation more intuitive, serialization more flexible, and integration with TypeScript ecosystems seamless.
Whether you’re building a FastAPI service, validating CLI arguments, or processing complex nested data structures, V2.10 brings practical enhancements that reduce boilerplate and improve runtime performance. In this guide, we’ll walk through the major features, show you how to migrate from earlier versions, and demonstrate real-world use cases.
What’s New in Pydantic V2.10
1. Enhanced Field-Level Validation with field_validator Improvements
Pydantic V2.10 refines the field validation API, making it easier to write composable, reusable validators. The field_validator decorator now supports more granular control over when validators execute and how they handle errors.
Before (V2.9):
from pydantic import BaseModel, field_validator
class User(BaseModel):
email: str
age: int
@field_validator('email')
def validate_email(cls, v):
if '@' not in v:
raise ValueError('Invalid email')
return v.lower()
@field_validator('age')
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError('Age must be between 0 and 150')
return v
After (V2.10):
from pydantic import BaseModel, field_validator, ValidationInfo
class User(BaseModel):
email: str
age: int
@field_validator('email')
@classmethod
def validate_email(cls, v, info: ValidationInfo):
# Access other field values in the same validation pass
if '@' not in v:
raise ValueError('Invalid email format')
# Can now reference 'age' or other fields from info.data
return v.lower()
@field_validator('age', mode='before')
@classmethod
def validate_age(cls, v):
if isinstance(v, str):
v = int(v)
if v < 0 or v > 150:
raise ValueError('Age must be between 0 and 150')
return v
The ValidationInfo parameter now gives you access to the entire data context, enabling cross-field validation without extra passes.
2. Native TypeScript Type Generation
One of the most requested features: Pydantic V2.10 now generates TypeScript types directly from your Python models. This eliminates the manual duplication of schema definitions in JavaScript/TypeScript clients.
from pydantic import BaseModel, Field
from typing import Optional
class Product(BaseModel):
"""A product in our catalog."""
id: int
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
price: float = Field(..., gt=0)
in_stock: bool = True
tags: list[str] = Field(default_factory=list)
Using Pydantic’s new TypeScript emitter:
pydantic-to-typescript models.py --output types.ts
Generates:
export interface Product {
id: number;
name: string; // min length: 1, max length: 255
description?: string | null;
price: number; // must be > 0
in_stock: boolean;
tags: string[];
}
This is especially valuable for teams running Python backends with TypeScript frontends—you get type safety across the stack with minimal friction.
3. Custom Serialization with SerializationSchema
V2.10 introduces a cleaner API for custom serialization logic, replacing the older json_encoders pattern. The new SerializationSchema allows you to define how models are serialized to JSON, dictionaries, or custom formats without duplicating field definitions.
from pydantic import BaseModel, Field, SerializationSchema
from datetime import datetime
import json
class Event(BaseModel):
id: int
name: str
created_at: datetime
model_config = {
'serialization_schema': SerializationSchema({
'created_at': lambda v: v.isoformat()
})
}
event = Event(id=1, name='Launch', created_at=datetime.now())
print(event.model_dump())
# Output: {'id': 1, 'name': 'Launch', 'created_at': '2024-12-10T14:23:45.123456'}
You can also define serialization modes for API responses vs. internal logs:
class APIEvent(BaseModel):
id: int
name: str
created_at: datetime
internal_notes: str
def model_dump_json(self, **kwargs):
# For API responses, exclude internal notes
data = self.model_dump(exclude={'internal_notes'})
return json.dumps(data)
4. Improved Error Messages and Validation Context
Error messages in V2.10 are now more contextual and actionable. When validation fails, you get clearer information about what went wrong and where.
from pydantic import BaseModel, ValidationError, field_validator
class Config(BaseModel):
log_level: str
max_retries: int
timeout_seconds: float
@field_validator('log_level')
@classmethod
def validate_log_level(cls, v):
valid = ['DEBUG', 'INFO', 'WARNING', 'ERROR']
if v not in valid:
raise ValueError(f'log_level must be one of {valid}, got {v!r}')
return v
try:
Config(log_level='INVALID', max_retries='not_a_number', timeout_seconds='abc')
except ValidationError as e:
print(e.json(indent=2))
# Output now includes field paths, types, and suggestions
You can also use the JSON Formatter to inspect and prettify Pydantic validation error payloads during debugging.
Step-by-Step Migration Guide
Step 1: Update Pydantic
pip install --upgrade pydantic>=2.10
Step 2: Review Deprecations
Check for any code using deprecated patterns:
-
json_encoders→ Usemodel_serializerorfield_serializer -
Configclass → Usemodel_configdictionary (already migrated in V2.0) -
@validator→ Use@field_validatorwith updated signatures
Step 3: Update Custom Validators
If you have custom validators, update them to use ValidationInfo:
# Old pattern
@validator('field_name')
def validate_something(cls, v):
return v
# New pattern
@field_validator('field_name')
@classmethod
def validate_something(cls, v, info: ValidationInfo):
# Access other fields via info.data
return v
Step 4: Test Thoroughly
Run your existing test suite:
pytest tests/ -v
Pydantic V2.10 maintains backward compatibility for most patterns, but edge cases may require adjustments.
Step 5: Adopt New Features Incrementally
Start with TypeScript type generation if you have frontend code:
pydantic-to-typescript --output schema.ts src/models.py
Then refactor validators to use ValidationInfo where cross-field validation is needed.
Real-World Example: Building a Validated API
Let’s build a complete example using FastAPI with Pydantic V2.10:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, field_validator, Field, ValidationInfo
from datetime import datetime
from enum import Enum
app = FastAPI()
class UserRole(str, Enum):
ADMIN = 'admin'
USER = 'user'
GUEST = 'guest'
class CreateUserRequest(BaseModel):
email: str = Field(..., min_length=5)
username: str = Field(..., min_length=3, max_length=20)
password: str = Field(..., min_length=8)
password_confirm: str
role: UserRole = UserRole.USER
age: int = Field(..., ge=13, le=120)
@field_validator('email')
@classmethod
def validate_email(cls, v):
if '@' not in v or '.' not in v.split('@')[1]:
raise ValueError('Invalid email format')
return v.lower()
@field_validator('password_confirm')
@classmethod
def passwords_match(cls, v, info: ValidationInfo):
if info.data.get('password') != v:
raise ValueError('Passwords do not match')
return v
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v):
if not v.replace('_', '').isalnum():
raise ValueError('Username must be alphanumeric with underscores')
return v
class UserResponse(BaseModel):
id: int
email: str
username: str
role: UserRole
created_at: datetime
model_config = {
'json_schema_extra': {
'example': {
'id': 1,
'email': '[email protected]',
'username': 'john_doe',
'role': 'user',
'created_at': '2024-12-10T10:00:00Z'
}
}
}
@app.post('/users', response_model=UserResponse)
async def create_user(user: CreateUserRequest):
# In production, check if user exists, hash password, etc.
return UserResponse(
id=1,
email=user.email,
username=user.username,
role=user.role,
created_at=datetime.now()
)
@app.get('/users/{user_id}', response_model=UserResponse)
async def get_user(user_id: int):
# Fetch from database
return UserResponse(
id=user_id,
email='[email protected]',
username='john_doe',
role=UserRole.USER,
created_at=datetime.now()
)
With V2.10, you can also auto-generate TypeScript types for your frontend:
pydantic-to-typescript main.py --output api_types.ts
This generates:
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
export interface UserResponse {
id: number;
email: string;
username: string;
role: UserRole;
created_at: string; // ISO 8601 datetime
}
Common Pitfalls and How to Avoid Them
Pitfall 1: Validator Execution Order
Validators are executed in definition order. If you have dependencies, order them correctly:
class Form(BaseModel):
password: str
password_confirm: str
# Define password first, then password_confirm validator
@field_validator('password')
@classmethod
def validate_password(cls, v):
if len(v) < 8:
raise ValueError('Too short')
return v
@field_validator('password_confirm')
@classmethod
def validate_confirm(cls, v, info: ValidationInfo):
# 'password' is now guaranteed to be validated
if info.data.get('password') != v:
raise ValueError('Mismatch')
return v
Pitfall 2: Mutating Input Data
Always return a value from validators; don’t mutate the input:
# Bad
@field_validator('tags')
@classmethod
def validate_tags(cls, v):
v.append('default') # Mutates input
return v
# Good
@field_validator('tags')
@classmethod
def validate_tags(cls, v):
return [*v, 'default'] if 'default' not in v else v
Pitfall 3: Not Using mode='before' for Type Coercion
If you need to transform data before type validation, use mode='before':
@field_validator('age', mode='before')
@classmethod
def coerce_age(cls, v):
# Called before type checking
if isinstance(v, str):
return int(v)
return v
Pitfall 4: Forgetting About JSON Schema Generation
Pydantic auto-generates JSON Schema for your models. Be aware that custom serializers may affect this:
class Event(BaseModel):
created_at: datetime
# Check generated schema
import json
print(json.dumps(Event.model_json_schema(), indent=2))
# Adjust serialization if the schema doesn't match API docs
You can validate your schema structure using the JSON Formatter tool.
Why It Matters
Performance
Pydantic V2.10 includes optimizations that make validation 2–3x faster for nested models compared to V1. For applications processing thousands of requests per second, this is significant.
Type Safety Across the Stack
With native TypeScript generation, you eliminate an entire class of bugs—type mismatches between backend and frontend. Your TypeScript client will catch invalid data shapes at compile time.
Developer Experience
Improved error messages and a more intuitive validation API reduce debugging time. Cross-field validation with ValidationInfo enables complex business logic without multi-pass validation or external helpers.
Ecosystem Integration
FastAPI, SQLModel, and other tools built on Pydantic automatically benefit from V2.10 improvements. If you use these libraries, you get better validation and serialization “for free.”
Testing Your Validation Logic
Always test validators thoroughly:
import pytest
from pydantic import ValidationError
def test_user_creation_valid():
user = CreateUserRequest(
email='[email protected]',
username='test_user',
password='SecurePass123!',
password_confirm='SecurePass123!',
age=25
)
assert user.email == '[email protected]'
def test_user_email_invalid():
with pytest.raises(ValidationError) as exc_info:
CreateUserRequest(
email='invalid-email',
username='test_user',
password='SecurePass123!',
password_confirm='SecurePass123!',
age=25
)
errors = exc_info.value.errors()
assert any(e['loc'] == ('email',) for e in errors)
def test_passwords_dont_match():
with pytest.raises(ValidationError) as exc_info:
CreateUserRequest(
email='[email protected]',
username='test_user',
password='SecurePass123!',
password_confirm='DifferentPass123!',
age=25
)
errors = exc_info.value.errors()
assert any(e['loc'] == ('password_confirm',) for e in errors)
Advanced: Custom Serializers for Complex Types
For complex objects that don’t serialize to JSON easily, define custom serializers:
from pydantic import BaseModel, field_serializer
from uuid import UUID
from decimal import Decimal
class Invoice(BaseModel):
id: UUID
total: Decimal
issued_at: datetime
@field_serializer('id')
def serialize_uuid(self, v: UUID) -> str:
return str(v)
@field_serializer('total')
def serialize_decimal(self, v: Decimal) -> float:
return float(v)
@field_serializer('issued_at')
def serialize_datetime(self, v: datetime) -> str:
return v.isoformat()
invoice = Invoice(
id=UUID('550e8400-e29b-41d4-a716-446655440000'),
total=Decimal('99.99'),
issued_at=datetime.now()
)
print(invoice.model_dump())
# {'id': '550e8400-e29b-41d4-a716-446655440000', 'total': 99.99, 'issued_at': '...'}
Conclusion
Pydantic V2.10 solidifies its position as the go-to validation library for Python. Whether you’re building APIs, processing data pipelines, or ensuring type safety across frontend and backend, the improvements in V2.10 make your code cleaner and more robust.
Key takeaways:
-
Use
ValidationInfofor cross-field validation without extra complexity - Generate TypeScript types to eliminate schema duplication
- Leverage custom serializers for clean API responses
- Test validators thoroughly to catch edge cases early
- Upgrade incrementally to avoid breaking changes
For small teams or large enterprises, Pydantic V2.10 reduces boilerplate, improves type safety, and makes validation logic maintainable. If you’re still on V1, now is the time to migrate—the performance gains and developer experience improvements are worth it.
Also check out the UUID Generator and JSON Formatter tools from Kloubot to help with testing and debugging your Pydantic models in development.