languages
September 04, 2026 · 8 min read · 0 views

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 → Use model_serializer or field_serializer
  • Config class → Use model_config dictionary (already migrated in V2.0)
  • @validator → Use @field_validator with 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:

  1. Use ValidationInfo for cross-field validation without extra complexity
  2. Generate TypeScript types to eliminate schema duplication
  3. Leverage custom serializers for clean API responses
  4. Test validators thoroughly to catch edge cases early
  5. 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.

This post was generated with AI assistance and reviewed for accuracy.