Skip to main content
FastAPI is designed around one core principle: validate all incoming data, process it internally, and return only the data that clients should see. This lesson builds a realistic Employee Management System that shows exactly why you need three distinct Pydantic models — one for what clients send, one for what your application works with internally, and one for what you expose in responses. By the end, you’ll understand how Field(), Query(), and Path() validators all fit together, and when to reach for the Annotated style.

The Three-Model Pattern

A real application keeps incoming data, internal business data, and outgoing response data strictly separated:

Request Processing Flow

Every request passes through multiple validation stages before reaching your business logic:

1. Path Parameter Validation

Path parameters identify a specific resource. Add Path() constraints using Annotated:
FastAPI validates that:
  • The value can be converted to int
  • It is greater than zero (gt=0)
If the client sends /employees/abc or /employees/-5, FastAPI returns a 422 error automatically — your function never runs.

2. Query Parameter Validation

Query parameters filter, search, sort, or paginate results:
Request:

3. Request Body Validation

When clients create or update resources, they send JSON in the request body:
Create a Request Model to validate it:
FastAPI automatically reads the JSON body, validates it, creates an EmployeeCreate instance, and passes it to your function. If validation fails, 422 is returned immediately.

4. Why Isn’t the Request Model Enough?

The request model represents only what the client is allowed to send. But your application usually needs to generate additional data automatically — data that should never come from the client:
  • Employee ID
  • Employee Code
  • Tax ID
  • Joining Date
  • Created Timestamp
That’s why you need a separate internal model.

5. Internal Model

After validation, your application creates its own complete working object:
The client never sends id, employee_code, tax_id, or joined_at — these are generated by your application logic.

6. Response Model

The application should not expose its internal model directly. Create a Response Model containing only the fields that clients should receive:
Although EmployeeInternal contains salary, employee_code, tax_id, and joined_at, the client only receives:

Validation Tools Reference

Field() — Request Body Validation

Query() — Query Parameter Validation

Path() — Path Parameter Validation

Common Validation Options


In Pydantic v2, the recommended way to write validation is with Annotated:
The older syntax (age: int = Field(gt=0, lt=100)) still works, but Annotated is the preferred style going forward.

model_config and from_attributes

When your API needs to return data from a database ORM (like SQLAlchemy), Pydantic needs to read attribute values from an object rather than a dictionary. Configure this with:
Without from_attributes=True, Pydantic reads values using dictionary keys (data["name"]). With it, Pydantic reads from object attributes (data.name) — which is how ORM objects work. Use model_validate() to convert an ORM object to your response model:
Best Practice: Use separate models for Request, Internal, and Response. Each has one responsibility — validation, processing, and safe exposure respectively. This keeps your application secure, flexible, and easy to maintain.