FastAPI & Pydantic Models

FastAPI is built on Pydantic: you describe request bodies, responses, and configuration as Python classes with type hints, and Pydantic parses and validates incoming data, serializes outgoing data, and generates the JSON Schema that becomes your OpenAPI documentation. One set of models gives you runtime validation, editor autocompletion, and API docs, the Python equivalent of what Zod does for TypeScript.

Pydantic v2, rewritten with a Rust core, is much faster than v1 and has a cleaner API (model_validate, field_validator, model_config). Designing models well (separate create, update, and read schemas, precise constraints, and explicit response models) is key to APIs that are safe, well documented, and easy to evolve.

TL;DR

Quick Example

Invalid input produces a structured 422 response pointing to the exact field (body.items.0.quantity), with no hand-written validation code.

Core Concepts

Request Models

Declaring a parameter typed as a BaseModel tells FastAPI to read it from the JSON body. Nested models, lists, dicts, unions, enums, and optional fields all work. Pydantic coerces compatible input by default (a string "42" to an int in lax mode). Use strict mode or strict types (StrictInt) where coercion is undesirable. Query and path parameters use the same type system via Query() and Path(), and FastAPI also supports query parameter models.

Field Constraints and Types

Constraints appear in the OpenAPI schema, so clients and generated SDKs see them too.

Validators

Response Models

Setting response_model=OrderRead, or annotating the return type, makes FastAPI:

  1. Validate the returned data against the model (catching bugs where you return the wrong shape).
  2. Filter output to the model's fields, so extra attributes, including sensitive ones, are dropped.
  3. Document the response schema in OpenAPI.

Options like response_model_exclude_none=True control serialization. Returning ORM objects works when the model has from_attributes=True.

Separate Schemas per Use Case

A common pattern:

For PATCH, use payload.model_dump(exclude_unset=True) to apply only the fields the client actually sent.

Discriminated Unions

For polymorphic payloads, use a Literal tag field and Field(discriminator="type"): Pydantic picks the right model directly, validation errors are clearer, and OpenAPI documents it with oneOf plus a discriminator.

Settings Management

pydantic-settings loads configuration from environment variables and .env files into typed, validated objects:

Best Practices

Never Return Raw ORM Objects Without a Response Model

Always declare a response model or return type, so FastAPI filters output. It prevents accidental exposure of password hashes, internal flags, or new columns added later.

Validate at the Boundary, Trust Internally

Put input constraints in request models so handlers receive clean data. Keep business rules that need database access (uniqueness, stock availability) in service code, returning clear errors.

Keep Models Separate From ORM Entities

API schemas and database models change for different reasons. Separate classes (or SQLModel used carefully) let you evolve the database without breaking the API contract. See API design.

Use Precise Types for Money and Time

Use Decimal for currency (never float), timezone-aware datetime, and UUID types. Document formats in field descriptions and examples.

Common Mistakes

One Model for Create, Update, and Read

Reusing a single model means clients can set server-managed fields (id, status, created_at), or every field is optional everywhere. Define separate schemas.

Mutable Defaults Without Field

In Pydantic, tags: list[str] = [] is safe, since defaults are copied, but be explicit with Field(default_factory=list) for clarity, and remember that the plain-Python mutable default pitfall still applies to dataclasses and regular classes.

Using Pydantic v1 Patterns on v2

orm_mode, .dict(), .parse_obj(), and @validator are v1 APIs. Their v2 equivalents are from_attributes, .model_dump(), .model_validate(), and @field_validator. Mixing them causes deprecation warnings or errors after upgrades.

FAQ

How does FastAPI validate request bodies?

It inspects endpoint parameters. A parameter typed as a Pydantic model is read from the JSON body and validated with model_validate. If validation fails, FastAPI returns a 422 response listing each error's location, message, and type, without calling your handler.

What does response_model do?

It validates your handler's return value against a schema, filters out fields not in that schema, serializes the result, and documents the response in OpenAPI. Annotating the return type (-> OrderRead) achieves the same thing in modern FastAPI.

How do I read Pydantic models from SQLAlchemy objects?

Set model_config = ConfigDict(from_attributes=True) on the response model. FastAPI (or OrderRead.model_validate(orm_obj)) then reads attributes from the ORM object. Load needed relationships eagerly to avoid lazy-loading issues in async code.

Should I use SQLModel?

SQLModel combines SQLAlchemy models and Pydantic schemas in one class, which reduces duplication for simple CRUD apps. For larger APIs, separate SQLAlchemy models and Pydantic schemas give more flexibility to evolve the database and the API independently.

Related Topics

References