Receiving unvalidated JSON payloads across public HTTP endpoints or parsing untyped database rows causes runtime type errors, subtle data corruption, and security vulnerabilities.
Pydantic v2 rewrites Python's data validation ecosystem on top of a high-performance compiled Rust engine (pydantic-core), offering 5x to 20x higher throughput, strict static type safety, and ergonomic model validation hooks.
Table of Contents
Step 1: Environment Setup & Virtual Environment
Pydantic v2 requires Python 3.8+ and comes with precompiled Rust wheels for Windows, Linux, and macOS.
Create an isolated virtual environment and install the required dependencies:
# Create virtual environment
python -m venv pydantic-env
# Activate virtual environment
# Windows (PowerShell): .\pydantic-env\Scripts\Activate.ps1
# Linux / macOS: source pydantic-env/bin/activate
# Install project dependencies
pip install "pydantic>=2.0" pydantic-settings email-validator
Step 2: Architecture & Pydantic v1 vs. v2 Core Architecture
In Pydantic v2, data validation and serialization execute directly in compiled Rust (pydantic-core), bypassing slow interpreted Python loops.
Field validation is separated into @field_validator for single fields and @model_validator for cross-field validation.
Pydantic v1 vs. Pydantic v2 Upgrades
| Feature | Pydantic v1 (Deprecated) | Pydantic v2 (Modern Standard) | Performance Impact |
|---|---|---|---|
| Validation Core | Interpreted Python loops | Compiled Rust engine (pydantic-core) | 5x to 20x faster validation speed |
| Dictionary Export | model.dict() | model.model_dump() | Direct zero-copy memory extraction |
| JSON Serialization | model.json() | model.model_dump_json() | Direct Rust SIMD JSON serializer |
| Field Validation | @validator | @field_validator | Strict static type checker compliance |
| Cross-Field Logic | @root_validator | @model_validator(mode='after') | Full instance validation lifecycle hooks |
Step 3: Step-by-Step Schema Implementation Breakdown (Component Breakdown)
Step 3.1: Defining Data Types and Field Constraints
Use Field() to attach metadata, minimum/maximum boundaries, regex patterns, and default values to individual schema properties:
from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
from datetime import datetime
from typing import Optional
class UserRegistrationSchema(BaseModel):
username: str = Field(..., min_length=3, max_length=30, description="Alphanumeric username")
email: EmailStr
password: str = Field(..., min_length=8)
confirm_password: str = Field(..., min_length=8)
age: int = Field(..., ge=18, le=120)
registered_at: datetime = Field(default_factory=datetime.utcnow)
Step 3.2: Implementing Custom Field Normalization
Use @field_validator with @classmethod to sanitize or normalize values (such as converting usernames to lowercase and checking alphanumeric characters):
# Custom Single-Field Validator
@field_validator("username")
@classmethod
def validate_and_normalize_username(cls, value: str) -> str:
if not value.isalnum():
raise ValueError("Username must contain only alphanumeric characters without spaces or symbols.")
return value.lower()
Step 3.3: Cross-Field Validation with Model Validator
Use @model_validator(mode='after') to inspect multiple fields simultaneously after their individual types have been validated:
# Cross-Field Model Validator
@model_validator(mode="after")
def verify_password_match(self) -> 'UserRegistrationSchema':
if self.password != self.confirm_password:
raise ValueError("Password confirmation does not match the provided password.")
return self
Step 4: How to Run and Verify Output
Execute the application from your terminal:
python test_schema.py
Expected terminal output verification:
Validated User Object:
{
"username": "alexdev2026",
"email": "alex@ilabacademy.com",
"age": 29,
"registered_at": "2026-08-25T00:30:00.000000Z"
}
Step 5: Complete, Working Final Code
Here is the complete, consolidated implementation ready for production testing. Save this script as main.py:
from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from datetime import datetime
from typing import Optional
# 1. User Validation Schema
class UserRegistrationSchema(BaseModel):
username: str = Field(..., min_length=3, max_length=30, description="Alphanumeric username")
email: EmailStr
password: str = Field(..., min_length=8)
confirm_password: str = Field(..., min_length=8)
age: int = Field(..., ge=18, le=120)
registered_at: datetime = Field(default_factory=datetime.utcnow)
@field_validator("username")
@classmethod
def validate_and_normalize_username(cls, value: str) -> str:
if not value.isalnum():
raise ValueError("Username must contain only alphanumeric characters.")
return value.lower()
@model_validator(mode="after")
def verify_password_match(self) -> 'UserRegistrationSchema':
if self.password != self.confirm_password:
raise ValueError("Password confirmation does not match password.")
return self
# 2. Production Settings Configuration Schema
class AppSettings(BaseSettings):
app_name: str = "iLab Academy API"
environment: str = Field(default="production", pattern="^(development|staging|production)$")
database_url: str = Field(..., min_length=15)
max_connections: int = Field(default=20, ge=1, le=100)
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)
if __name__ == "__main__":
payload = {
"username": "AlexDev2026",
"email": "alex@ilabacademy.com",
"password": "SuperSecretPassword123!",
"confirm_password": "SuperSecretPassword123!",
"age": 29
}
user = UserRegistrationSchema(**payload)
print("Validated User Object:")
print(user.model_dump_json(indent=2, exclude={"password", "confirm_password"}))
Step 6: Production Hardening, Edge Cases & Security
- BaseSettings Moved Package: In Pydantic v2, BaseSettings is no longer in the core library. Install pydantic-settings via pip install pydantic-settings.
- Fast JSON Serialization: Prefer model.model_dump_json() over json.dumps(model.model_dump()) to leverage Rust SIMD JSON serialization.
- Frozen Immutable Models: Use model_config = ConfigDict(frozen=True) to make models hashable and thread-safe.
Step 7: Frequently Asked Questions & Troubleshooting
Q: What is the difference between mode='before' and mode='after' in @model_validator?
A: mode='before' receives raw input data as a Python dict before Pydantic performs any type coercion. mode='after' receives the fully validated model instance.
Q: How do I exclude sensitive fields from serialization?
A: Pass exclude={'password'} into model.model_dump() or model.model_dump_json().
0 Comments