Validation Pipeline
Pydantic is not just a type check. Inputs can arrive as Python objects, JSON strings, or string-heavy configuration sources, and those inputs pass through coercion, strict checks, validators, and serializers before they become model instances or output payloads.
Quick takeaway: the useful pipeline is `input -> coercion/strict check -> validators -> model or typed value -> serializers -> output`. Keep input rules and output rules separate if you want durable DTOs.
Pipeline Picture
Inputs Come in Different Modes
model_validate()/validate_python()for existing Python objectsmodel_validate_json()/validate_json()for raw JSON- settings and ingestion paths that are often string-heavy
Strict Mode vs Lax Mode
Pydantic often defaults to accepting inputs that can be reasonably parsed. That is useful, but it is a real design choice.
from datetime import date
from pydantic import TypeAdapter, ValidationError
adapter = TypeAdapter(date)
try:
adapter.validate_python("2026-03-02", strict=True)
except ValidationError as exc:
print("strict python input:", exc.errors()[0]["type"])
print(
"strict json input:",
adapter.validate_json('"2026-03-02"', strict=True),
)Strict JSON behavior can differ from strict Python-input behavior. The date example above is a useful reminder that transport format matters to validation policy.
Validators and Serializers Do Different Jobs
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
class Invoice(BaseModel):
model_config = ConfigDict(strict=False)
cents: int
currency: str
@field_validator("currency")
@classmethod
def normalize_currency(cls, value: str) -> str:
normalized = value.upper()
if normalized not in {"USD", "KRW"}:
raise ValueError("unsupported currency")
return normalized
@field_serializer("cents")
def serialize_cents(self, value: int) -> str:
return f"{value / 100:.2f}"
invoice = Invoice.model_validate({"cents": "2500", "currency": "krw"})
print(invoice)
print(invoice.model_dump())
print(invoice.model_dump(mode="json"))Use validators to normalize and enforce input rules. Use serializers to shape output contracts. They can attach to the same field, but they should not carry the same responsibilities.
Read ValidationError as Structured Data
ValidationError.errors()gives structured error entries.- FastAPI builds request-validation responses from that structure.
- Error location, type, and input value are often more useful than the message text alone.
Checklist
Set strictness per boundary
Public APIs, internal event ingestion, and environment parsing often deserve different coercion policies.
Keep validators focused
Validators should normalize and validate, not perform external I/O or orchestrate domain workflows.
Use serializers for output contracts
Output shape belongs in serializers or DTO layers, not in ad hoc response assembly everywhere.
Read errors structurally
Use `loc`, `type`, and `input` when you build good error handling or debugging workflows.