Hello! Welcome back to the course.
In the last lesson, we successfully defined our database structure by creating SQLAlchemy ORM models for User and Post in the models.py file. Those classes are the blueprint for our database tables.
Today, we're going to build the other side of the data-handling coin. The learning outcome for this lesson is to create distinct Pydantic schemas for data validation (create) and data representation (read). While our ORM models define how data is stored, Pydantic schemas will define how data is shaped for our API, both for incoming requests and outgoing responses.
1. The "Why": Separating API Schemas from Database Models
Before we write any code, it's crucial to solidify why we need this separation. Why not just use our SQLAlchemy models everywhere?
The core reason is separation of concerns. An API request to create a post should look different from the API response that represents a stored post.
- For Creating (Input): A client should only provide the data they are responsible for, like
titleandcontent. They should not be able to specify anidor acreated_attimestamp, as these are generated by our system. - For Reading (Output): When a client requests a post, our response should include the system-generated fields like
idandcreated_at. We also might want to hide sensitive information from the database model (e.g., a user's hashed password) in the API response.
This video from Corey Schafer provides a great explanation of this principle.
Python FastAPI Tutorial (Part 4): Pydantic Schemas - Request and Response Validation
To start, watch this short segment from the 'Python FastAPI Tutorial' by Corey Schafer. It clearly explains the role of Pydantic schemas and why they are kept separate from database models.
Watch the video from 00:50 to 02:28. Pay attention to the distinction made between what the client provides (data for the API) and what the system stores (data for the database).
2. The "Base, Create, Read" Pattern
A common and highly effective pattern is to define a Base schema with shared fields, and then create other schemas that inherit from it for specific operations. This keeps our code DRY (Don't Repeat Yourself).
We will create a new file in your project directory named schemas.py.
The process will look like this:
PostBase: Will contain fields common to both creating and reading a post (title,content).PostCreate: Will inherit fromPostBase. It defines the shape of data for aPOSTrequest to create a new post.PostRead(orPostResponse): Will inherit fromPostBaseand add the fields generated by the database, likeidandcreated_at.
Let's watch how this is done.
Python FastAPI Tutorial (Part 4): Pydantic Schemas - Request and Response Validation
The following clip from the same tutorial demonstrates this pattern in practice. You'll see how to create a base schema and then extend it for creation and response models.
Watch from 03:20 to 07:10. Observe how: A new schemas.py file is created. A PostBase class is defined with the common fields (title, content, author). A PostCreate class inherits from PostBase to define the input model.
3. Handling Nested Data and Database-Generated Fields
Our API responses won't just be flat objects. A post has an author, and a user can have multiple posts. Our schemas need to reflect these relationships.
- The
PostReadschema should include details about itsauthor. - The
UserReadschema should include a list of that user'sposts.
This means we need to create schemas for our User model as well. Critically, the UserRead schema must not include the hashed_password from the ORM model.
Furthermore, our "read" schemas will be created from the SQLAlchemy ORM objects we fetch from the database. By default, Pydantic expects dictionaries, not class instances. We need to tell Pydantic to read data from object attributes (e.g., my_post_object.title). This is done with a special configuration.
This final video segment puts it all together.
Python FastAPI Tutorial (Part 4): Pydantic Schemas - Request and Response Validation
This last clip shows how to complete the response model by adding database-generated fields and, most importantly, how to configure the schema to work with SQLAlchemy's ORM objects.
Watch from 07:10 to 10:23. Focus on two key points: The PostResponse schema adds id and date_posted fields. The model_config = ConfigDict(from_attributes=True) line. This is the magic that allows Pydantic to create a schema from a SQLAlchemy model instance. In older versions of Pydantic, this was class Config: orm_mode = True.
4. Hands-On: Creating schemas.py
Now, let's apply everything we've learned to our project. Create the schemas.py file and add the following code. I've included schemas for both Post and User to fully model our application's data flow.
# schemas.py
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import List
# ===============================================
# Post Schemas
# ===============================================
# Base schema for a Post - contains shared attributes
class PostBase(BaseModel):
title: str
content: str
# Schema for creating a Post (input)
# Inherits from PostBase, no extra fields needed for creation
class PostCreate(PostBase):
pass
# Forward declaration for the UserRead schema to handle circular dependencies
class UserRead(BaseModel):
id: int
email: str
# This configuration allows Pydantic to map data from ORM models
model_config = ConfigDict(from_attributes=True)
# Schema for reading a Post (output)
# Includes fields that are generated by the database
class PostRead(PostBase):
id: int
created_at: datetime
author_id: int
author: UserRead # Nested schema to show author details
# This configuration allows Pydantic to map data from ORM models
# It tells Pydantic to read data from attributes, not just dict keys
model_config = ConfigDict(from_attributes=True)
# ===============================================
# User Schemas
# ===============================================
# Base schema for a User
class UserBase(BaseModel):
email: str
# Schema for creating a User (input for registration)
# Includes password, which is required for creation but not for reading
class UserCreate(UserBase):
password: str
# We need to update the UserRead schema to include the list of posts
# Pydantic v2's `model_rebuild()` is a clean way to handle this after all models are defined
UserRead.model_rebuild()
Key elements in this file:
PostBase&UserBase: Capture the common fields, keeping our code DRY.PostCreate&UserCreate: Define the exact structure for incoming API requests.UserCreateincludes apassword, butPostCreatedoes not need any extra fields.PostRead&UserRead: Define the structure of our API responses. They include database-generated fields likeidandcreated_at.model_config = ConfigDict(from_attributes=True): This is the crucial link. It instructs Pydantic to create these schema instances from your SQLAlchemy model objects (UserandPostfrommodels.py).- Nested Schemas:
PostReadcontains a fieldauthor: UserRead, andUserReadcontainsposts: List[PostRead]. This powerful feature allows you to return rich, structured JSON responses that reflect your database relationships. - Forward Declaration &
model_rebuild(): Notice howPostReadreferencesUserRead, andUserReadshould referencePostRead. This creates a circular dependency. We initially defineUserReadwithout thepostsfield soPostReadcan be defined. Then,UserRead.model_rebuild()is called to update its definition afterPostReadis known. This resolves the circle.
For another clear, text-based example of this Base/Create/Response pattern, you can refer to the following article.
Pydantic in Action: Integrating with FastAPI and SQLAlchemy
The article 'Pydantic in Action' provides a concise, text-based example of this exact pattern, which is great for reinforcement.
Read the section titled 'Handling Data Flow: Create, Read, Update Models'. Notice how they use BlogBase, BlogCreate, and BlogResponse to achieve the same separation of concerns.
Conclusion
Excellent work! You have now defined the complete data contract for your API. You have SQLAlchemy models defining the database layer and Pydantic schemas defining the API layer. This clean separation is a hallmark of a well-structured FastAPI application.
Key Takeaways:
- Pydantic schemas define the shape and validation rules for your API's request and response data.
- Separating schemas from ORM models provides flexibility and security.
- The Base, Create, and Read pattern is a best practice for organizing schemas based on their purpose.
- The
model_configwithfrom_attributes=Trueis essential for creating Pydantic schemas from SQLAlchemy model instances. - Nested schemas allow you to represent database relationships in your JSON responses.
In the next lesson, we will finally bring these two pieces together. You will learn how to write a dependency to manage database sessions for each request. This will give our API endpoints the ability to connect to the database, execute queries, and use our new schemas to return the results.