Technical
Pydantic for API Contract Enforcement
APIs fail in production because a field was missing, had the wrong type, or was stringly typed when it should have been an enum. Pydantic catches these at the boundary, before malformed data reaches your business logic. Here is how I use it as a contract enforcement layer.
The Contract Idea
Every endpoint has an implicit contract: these fields must be present, they must have these types, some have constraints. Pydantic makes that contract explicit and enforces it automatically.
A Real Example
from pydantic import BaseModel, EmailStr, Field
from typing import Literal
class SubscriberCreate(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100)
locale: Literal['en-US', 'fr-FR', 'es-ES'] = 'en-US'
age: int = Field(ge=13, le=120)Four fields, every one validated. A request missing email fails immediately with a 422 response. A request with locale: 'jp' fails because it is not in the Literal list. A request with age: 12 fails because it is below the minimum.
What You Get for Free
- Type coercion:
age: '25'becomesage: 25 - Format validation:
email: 'not-an-email'fails - Enum enforcement: only allowed locales accepted
- Range checks: age stays sane
- Auto-generated error messages: the client sees exactly what was wrong
All of that from five extra lines in a model definition.
The Response Side
Pydantic also shapes responses. If the database row has 30 fields but the API should only return 5, I define a response model with just those 5. FastAPI filters automatically:
class SubscriberResponse(BaseModel):
id: str
email: EmailStr
created_at: datetime
@router.get('/{id}', response_model=SubscriberResponse)
async def get_subscriber(id: str):
return db.get_item(id) # returns 30 fields, FastAPI filters to 3Leaking internal fields is a common bug. Response models prevent it by design.
Computed Fields
Pydantic can derive fields on the fly:
from pydantic import computed_field
class Article(BaseModel):
body: str
@computed_field
@property
def reading_time_minutes(self) -> int:
return max(1, len(self.body.split()) // 200)The response includes reading_time_minutes without storing it in the database.
Why This Matters
Without Pydantic, validation happens in multiple places inconsistently. Some endpoints check email format, others do not. Some accept extra fields, others reject them. Pydantic makes the validation rule the same everywhere because it lives in the model, not the route.
Integration With OpenAPI
FastAPI reads Pydantic models to generate the OpenAPI schema. The docs at /docs reflect the actual validation rules. Frontend developers can generate TypeScript types from that schema. One definition becomes the contract enforced on both sides.
See the Pydantic documentation for the full set of validators and the FastAPI integration guide for response shaping.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read