This lesson on Why Pydantic for API Models is hands-on and example-driven. You will build declarative, type-annotated data contracts that automatically validate and serialize API payloads using Pydantic. You will implement built-in constraints, specialized field types, and custom multi-stage field and model validators to guarantee data integrity before processing incoming requests.
What You'll Be Able To Do
- Define structured data schemas by subclassing BaseModel and applying specialized types like EmailStr and SecretStr.
- Validate incoming dictionaries or JSON payloads programmatically using model_validate() and inspect detailed validation error messages.
- Implement single-field sanitization and pre-coercion logic using @field_validator with before and after execution modes.
- Construct multi-field validation rules and transformations using @model_validator to enforce complex business logic across model boundaries.
- Attach descriptive metadata and default constraints to model attributes using Field().
Detailed Concept Walkthrough
1. Declarative Modeling with BaseModel
Pydantic replaces manual validation logic by pairing standard Python type hints with specialized validation classes to enforce schema boundaries at runtime.
- Mechanism: Models are declared by subclassing BaseModel and defining class attributes with type annotations, which Pydantic inspects to construct runtime parsing routines.
- Under the Hood: Pydantic converts and casts matching types (such as string-encoded numbers to integers) while enforcing format constraints for specialized types like EmailStr and redacting sensitive values via SecretStr.
- Best Practice: Annotate fields with Field() to inject validation rules, default factory values, and OpenAPI schema metadata directly alongside the type definitions.
from pydantic import BaseModel, EmailStr, SecretStr, Field
class User(BaseModel):
id: int
name: str = Field(..., min_length=2, description="Full legal name")
email: EmailStr
password: SecretStr
is_active: bool = True
Key Takeaway: Subclassing BaseModel establishes a strict data contract that parses, casts, and validates input payloads automatically upon object creation.
2. Runtime Validation and Error Inspection
Pydantic parses raw input dictionaries against defined model constraints and produces structured, fail-fast error reports upon constraint violations.
- Execution Flow: Calling Model.model_validate(raw_dict) triggers synchronous parsing and validation across every field definition, returning an initialized model instance on success.
- Under the Hood: When an invalid payload is passed, Pydantic collects all failing field paths, error types, and contextual messages into a single ValidationError rather than halting at the first failure.
- Best Practice: Catch ValidationError during external data ingestion to extract structured error details (such as missing fields or format failures) for client responses.
from pydantic import ValidationError
raw_data = {"id": 1, "name": "A", "email": "invalid-email", "password": "secret123"}
try:
user = User.model_validate(raw_data)
except ValidationError as err:
# Inspect structured validation failures
print(err.errors())
Key Takeaway: The model_validate() method guarantees complete schema enforcement and exposes detailed diagnostic error lists on failure.
3. Custom Validation with Field Validators
The @field_validator decorator intercepts individual field values to execute custom normalization, regex pattern matching, or type coercions before or after standard validation.
- Execution Flow: Applying @field_validator with mode='before' runs custom logic on the raw input prior to standard Pydantic type coercion, allowing manual pre-processing such as integer-to-enum mapping.
- Mechanism: Applying @field_validator with mode='after' operates on the type-coerced value, allowing custom domain assertions like regex pattern matching on verified types.
- Syntax Rule: Methods decorated with @field_validator must be class methods using the @classmethod decorator, accept cls and the field value as arguments, and explicitly return the final value.
import re
from pydantic import BaseModel, field_validator
class Profile(BaseModel):
username: str
role_code: str
@field_validator("role_code", mode="before")
@classmethod
def coerce_role(cls, v: int | str) -> str:
# Coerce integer code to standard string role
return "admin" if v == 1 else "user"
@field_validator("username", mode="after")
@classmethod
def validate_alphanumeric(cls, v: str) -> str:
if not re.match(r"^[a-zA-Z0-9_]+$", v):
raise ValueError("Username must be alphanumeric")
return v
Key Takeaway: Use field validators with mode='before' for raw type coercion and mode='after' for granular domain assertions on specific fields.
4. Cross-Field Validation with Model Validators
The @model_validator decorator evaluates inter-field dependencies and executes whole-model transformations that individual field validators cannot resolve.
- Mechanism: Model validators configured with mode='after' run after individual field validations succeed and receive the partially initialized model instance (self).
- Execution Flow: Cross-field checks (such as verifying that a password does not contain the username) access all model attributes simultaneously on self.
- Best Practice: Use model validators to perform post-validation transformations, such as computing derived attributes or hashing sensitive credentials before object finalization.
import hashlib
from pydantic import BaseModel, SecretStr, model_validator
class SignupRequest(BaseModel):
username: str
password: SecretStr
password_hash: str | None = None
@model_validator(mode="after")
def validate_and_hash(self):
raw_pw = self.password.get_secret_value()
if self.username.lower() in raw_pw.lower():
raise ValueError("Password cannot contain username")
self.password_hash = hashlib.sha256(raw_pw.encode()).hexdigest()
return self
Key Takeaway: Model validators with mode='after' provide access to the full model instance for cross-field relational rules and whole-instance data transformations.
Topics Covered in Why Pydantic for API Models
- Pydantic Fundamentals and Use Cases (0:31 - 1:40) — Explore how Pydantic extends standard dataclasses with runtime data validation and serialization.
- Creating Models with BaseModel (1:40 - 3:36) — Define structured schemas using BaseModel, specialized types, and Field metadata.
- Validation and Error Inspection (3:36 - 4:22) — Validate input dictionaries using model_validate and handle structured validation errors.
- Field Validators (4:22 - 6:08) — Implement custom field-level constraints and pre-coercion logic using field_validator modes.
- Cross-Field Model Validation (6:08 - 7:22) — Apply model_validator to enforce cross-field business logic and execute data transformations.
FastAPI Cheat Sheet
-
class ModelName(BaseModel):— Defines a validated data model schemaclass User(BaseModel): name: str -
Field(..., description="...")— Configures field metadata, defaults, and constraintsage: int = Field(gt=0, description="User age") -
EmailStr / SecretStr— Enforces email formatting and redacts sensitive stringsemail: EmailStr password: SecretStr -
Model.model_validate(dict_data)— Validates a dictionary against the schemauser = User.model_validate({"name": "Alex"}) -
@field_validator("field", mode="before")— Validates or coerces raw input before parsing@field_validator("role", mode="before") @classmethod def coerce(cls, v): return str(v) -
@field_validator("field", mode="after")— Validates a single field after type coercion@field_validator("name", mode="after") @classmethod def check(cls, v): return v.strip() -
@model_validator(mode="after")— Validates relationships across multiple fields@model_validator(mode="after") def verify_match(self): return self
Comparison Table
| Validator Type | Evaluation Timing | Primary Purpose |
|---|---|---|
| Field Validator (before) | Prior to standard type coercion | Raw input cleaning and coercion |
| Field Validator (after) | After single-field type coercion | Single-attribute constraints and regex rules |
| Model Validator (after) | After all fields validated | Cross-field business logic and hashing |
Common Pitfalls
- Mistake: Using standard str for passwords or sensitive API tokens. Avoid: Use SecretStr to prevent plaintext exposure in logs and error outputs.
- Mistake: Forgetting to return the validated value from a field validator method. Avoid: Always explicitly return the final value at the end of every field validator.
- Mistake: Performing cross-field checks inside a single field validator. Avoid: Use model validator with mode after to inspect relational constraints between multiple fields.
- Mistake: Omitting classmethod on field validator definitions. Avoid: Always decorate field validator methods with classmethod and accept cls as the first parameter.
FAQs
- Why use Pydantic BaseModel instead of standard Python dataclasses? Standard dataclasses perform static type hinting without runtime data validation or coercion, whereas Pydantic enforces schema constraints and parses incoming payloads dynamically.
- What is the difference between mode='before' and mode='after' in field validators? Mode 'before' processes raw, uncoerced input data for pre-processing, while mode 'after' operates on the value after Pydantic has validated its primary type.
- How does SecretStr protect sensitive credentials during logging? SecretStr automatically masks its content in string representations and logs, requiring an explicit call to get_secret_value() to access the raw plaintext string.
- What happens when model_validate encounters invalid data? It raises a ValidationError containing a list of all offending fields, constraint types, and diagnostic error messages.