FastAPI Complete Course Module 4: Request & Response Models

FastAPI Complete Course Module 4: Request & Response Models

AI Reading

Quick summary of this article

This module teaches how to use Pydantic models in FastAPI to validate incoming data and structure outgoing responses. Request models ensure client data is correct and safe, while response models control what information is sent back, improving security and consistency.

  • Pydantic models define data structure using Python type hints, automatically validating types and converting data (e.g., string "123" to integer 123).
  • Request models group client data into a single object; FastAPI validates it automatically and returns a 422 error if data is invalid.
  • Response models filter outgoing data, hiding sensitive fields like passwords and ensuring responses match a defined structure.
  • Field validation includes built-in options (e.g., string length limits, integer ranges, email format) and custom validators for complex rules like password matching.
  • Optional fields (using Optional[type] with None default) allow clients to omit data, perfect for partial updates, while default values provide fallbacks for missing fields.

Introduction

Welcome to Module 4 of the FastAPI Complete Course. In the previous modules, you learned how to set up FastAPI, define basic path operations, and handle path and query parameters. Now, we step into one of the most powerful features of FastAPI: Request & Response Models.

When building professional APIs, you cannot rely on raw data. You need to ensure that the data sent by clients is valid, structured, and safe. Similarly, the data you return to clients must be predictable and well-formed. This is where Pydantic comes in.

FastAPI uses Pydantic models to define the shape of your data. By the end of this chapter, you will be able to:

  • Understand what Pydantic is and why it is essential for FastAPI.
  • Create request models to validate incoming data.
  • Define response models to control what data is sent back.
  • Use field validation, default values, and optional fields effectively.
  • Avoid common pitfalls that beginners face.

The primary focus keyword for this chapter is FastAPI Request & Response Models. Let’s dive deep into this topic and build a solid foundation for your API development journey.

Introduction to Pydantic

Pydantic is a Python library that provides data validation and settings management using Python type annotations. It is the backbone of FastAPI’s data handling. When you define a class that inherits from BaseModel, you are creating a Pydantic model.

Why Pydantic?

  • Type Safety: Pydantic enforces type hints at runtime. If a client sends a string where an integer is expected, Pydantic raises a validation error.
  • Automatic Parsing: It can convert data types automatically. For example, a string "123" can be parsed into an integer 123 if the field is declared as int.
  • JSON Schema: Pydantic models automatically generate JSON Schema, which FastAPI uses to create interactive API documentation (Swagger UI).
  • Nested Models: You can define complex data structures by nesting Pydantic models inside each other.

Installing Pydantic

If you are using FastAPI, Pydantic is already installed as a dependency. However, you can install it separately if needed:

pip install pydantic

Let’s create a simple Pydantic model to understand its basic structure.

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    email: str

In this example:

  • Person inherits from BaseModel.
  • We define three fields: name (string), age (integer), and email (string).
  • When you create an instance of Person, Pydantic validates the data automatically.
# Valid data
p = Person(name="Alice", age=30, email="alice@example.com")
print(p)  # Output: name='Alice' age=30 email='alice@example.com'

# Invalid data (age is a string)
try:
    p = Person(name="Bob", age="thirty", email="bob@example.com")
except Exception as e:
    print(e)  # Validation error

This is the foundation of FastAPI Request & Response Models. Now, let’s see how to use these models in a FastAPI application.

Creating Request Models

In FastAPI, a request model defines the structure of the data you expect from the client. Instead of accepting individual parameters, you can group them into a Pydantic model and use it as a parameter in your path operation.

Basic Request Model Example

Let’s create an API endpoint that accepts user registration data.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserRegistration(BaseModel):
    username: str
    email: str
    password: str

@app.post("/register")
async def register_user(user: UserRegistration):
    # Process the user data
    return {"message": f"User {user.username} registered successfully!"}

Explanation:

  • We define a Pydantic model UserRegistration with three fields.
  • In the path operation function, we declare a parameter user of type UserRegistration.
  • FastAPI automatically reads the request body, validates it against the model, and passes the validated data to the function.
  • If the client sends invalid data (e.g., missing fields or wrong types), FastAPI returns a 422 Unprocessable Entity error with details.

Testing with Swagger UI

Run the FastAPI application:

uvicorn main:app --reload

Open http://127.0.0.1:8000/docs in your browser. You will see the Swagger UI with the /register endpoint. Click on it, then click “Try it out”. You can send a JSON request like:

{
  "username": "john_doe",
  "email": "john@example.com",
  "password": "securepass123"
}

The API will return a success message. This is the power of FastAPI Request & Response Models in action.

Response Models

Response models allow you to control what data is sent back to the client. You can use a Pydantic model to define the structure of the response, ensuring consistency and security (e.g., hiding sensitive fields like passwords).

Basic Response Model Example

Let’s extend the previous example to return a structured response.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserRegistration(BaseModel):
    username: str
    email: str
    password: str

class UserResponse(BaseModel):
    username: str
    email: str
    message: str

@app.post("/register", response_model=UserResponse)
async def register_user(user: UserRegistration):
    # In a real app, you would save the user to a database
    return UserResponse(
        username=user.username,
        email=user.email,
        message="User registered successfully!"
    )

Key points:

  • We define a second Pydantic model UserResponse with the fields we want to expose.
  • We use the response_model parameter in the decorator to specify the response type.
  • FastAPI will automatically filter and validate the response data. If you accidentally include extra fields, they will be removed.
  • Notice that the password field is not included in UserResponse, so it will never be sent to the client.

Using response_model with List

You can also return lists of models.

from typing import List

class Item(BaseModel):
    name: str
    price: float

@app.get("/items", response_model=List[Item])
async def get_items():
    return [
        Item(name="Laptop", price=999.99),
        Item(name="Mouse", price=19.99)
    ]

This endpoint returns a JSON array of items, each conforming to the Item model.

Field Validation

Pydantic provides powerful field validation capabilities. You can use Python’s type hints, Pydantic’s built-in validators, or custom validators to enforce rules on your data.

Built-in Validators

Pydantic supports many types that automatically validate data:

  • constr(min_length=..., max_length=...) for strings
  • conint(ge=..., le=...) for integers
  • confloat(ge=..., le=...) for floats
  • EmailStr for email validation (requires pydantic[email])
from pydantic import BaseModel, EmailStr, conint, constr

class UserProfile(BaseModel):
    username: constr(min_length=3, max_length=20)
    age: conint(ge=18, le=120)
    email: EmailStr

# Valid data
profile = UserProfile(username="alice", age=25, email="alice@example.com")

# Invalid data (age too low)
try:
    profile = UserProfile(username="bob", age=15, email="bob@example.com")
except Exception as e:
    print(e)  # Validation error

Explanation:

  • constr(min_length=3, max_length=20) ensures the username is between 3 and 20 characters.
  • conint(ge=18, le=120) ensures age is between 18 and 120.
  • EmailStr validates that the string is a valid email format.

Custom Validators

For more complex validation, you can use the @validator decorator.

from pydantic import BaseModel, validator

class PasswordCheck(BaseModel):
    password: str
    confirm_password: str

    @validator('confirm_password')
    def passwords_match(cls, v, values):
        if 'password' in values and v != values['password']:
            raise ValueError('Passwords do not match')
        return v

# Valid data
check = PasswordCheck(password="secret123", confirm_password="secret123")

# Invalid data
try:
    check = PasswordCheck(password="secret123", confirm_password="different")
except Exception as e:
    print(e)  # Validation error

Key points about custom validators:

  • The first argument is cls (the class).
  • The second argument is the value of the field being validated.
  • The values parameter (optional) gives access to other fields.
  • You must return the validated value or raise a ValueError.

Default Values

Default values make fields optional in the sense that if the client does not provide them, the model uses the default. However, they are still considered required in the JSON Schema unless you explicitly mark them as optional.

Setting Default Values

from pydantic import BaseModel
from typing import Optional

class Product(BaseModel):
    name: str
    price: float
    description: str = "No description provided"
    in_stock: bool = True

# Client sends only name and price
product = Product(name="Widget", price=9.99)
print(product.description)  # Output: No description provided
print(product.in_stock)     # Output: True

In this example:

  • description defaults to "No description provided".
  • in_stock defaults to True.
  • The client can still override these values by sending them in the request.

Default Values with Factory Functions

For mutable defaults like lists or dictionaries, you must use default_factory.

from pydantic import BaseModel
from typing import List
from datetime import datetime

class Order(BaseModel):
    items: List[str] = []
    created_at: datetime = datetime.now()

# This is WRONG for mutable types
class WrongOrder(BaseModel):
    items: List[str] = []  # This will be shared across all instances!

Instead, use:

from pydantic import Field

class CorrectOrder(BaseModel):
    items: List[str] = Field(default_factory=list)
    created_at: datetime = Field(default_factory=datetime.now)

Explanation:

  • default_factory calls the function every time a new instance is created.
  • This ensures each instance gets its own list or timestamp.

Optional Fields

Optional fields allow the client to omit values entirely. In Pydantic, you use Optional[type] from the typing module, and the field’s default is None.

Defining Optional Fields

from pydantic import BaseModel
from typing import Optional

class UserUpdate(BaseModel):
    username: Optional[str] = None
    email: Optional[str] = None
    age: Optional[int] = None

In this model:

  • All fields are optional. The client can send only the fields they want to update.
  • If a field is not provided, its value will be None.
  • This is perfect for PATCH endpoints where you only update specific fields.

Difference Between Optional and Default

There is a subtle difference:

  • Optional[str] = None: The field can be omitted entirely. If omitted, it becomes None.
  • str = "default": The field is still required in the JSON Schema, but if omitted, it gets the default value.

If you want a field to be truly optional (not required in the schema), use Optional with None as default.

from pydantic import BaseModel
from typing import Optional

class Config(BaseModel):
    debug: bool = False          # Required in schema, but has default
    log_level: Optional[str] = None  # Not required in schema

In the Swagger UI, debug will appear as a required field (though it has a default), while log_level will be optional.

Common Mistakes

Here are frequent errors beginners make with FastAPI Request & Response Models:

  1. Forgetting to use response_model: If you don’t specify response_model, FastAPI returns the raw Pydantic object, which might include sensitive fields.
  2. Mutating default values: Using mutable default values like [] or {} directly in the model definition. Always use Field(default_factory=...).
  3. Confusing Optional with default: Using Optional[str] without setting a default (= None) still makes the field required in the schema.
  4. Not using EmailStr for emails: Relying on plain str for email fields means no validation. Use EmailStr from pydantic.
  5. Over-validating: Adding too many custom validators can make code hard to read. Use built-in validators when possible.

Practice Task

Now it’s your turn. Create a FastAPI application that manages a library of books. Your task:

  1. Define a Pydantic model BookRequest for creating a book with fields:
    • title: string, min length 1, max length 100
    • author: string, min length 3
    • year: integer, between 1900 and current year
    • isbn: optional string, if provided must be exactly 13 characters
  2. Define a response model BookResponse that includes all fields except any internal IDs (if you add one).
  3. Create a POST endpoint /books that accepts BookRequest and returns BookResponse with a generated ID.
  4. Create a GET endpoint /books/{book_id} that returns a single book.
  5. Use an in-memory list to store books.

Test your API using Swagger UI. Ensure that validation errors are returned for invalid data.

Summary

In this chapter, you learned the core concepts of FastAPI Request & Response Models:

  • Pydantic is the data validation library that powers FastAPI’s request and response handling.
  • Request models define the structure of incoming data, ensuring it is valid before your code processes it.
  • Response models control what data is sent back, improving security and consistency.
  • Field validation using built-in types and custom validators enforces business rules.
  • Default values make fields optional while providing sensible defaults.
  • Optional fields allow clients to omit data entirely, perfect for partial updates.

Mastering these concepts is crucial for building robust, production-ready APIs. You now have the tools to handle complex data scenarios with confidence.

FAQs

1. What is the difference between Pydantic and Python dataclasses?

Pydantic models provide automatic validation, JSON Schema generation, and parsing capabilities that Python dataclasses lack. While dataclasses are great for simple data containers, Pydantic is specifically designed for data validation in APIs.

2. Can I use the same model for both request and response?

Yes, but it is not recommended. Request models often contain fields like passwords that should not be exposed in responses. It is better practice to define separate models for requests and responses.

3. How do I handle nested JSON objects?

Pydantic supports nested models. You can define a model with fields that are themselves Pydantic models. FastAPI will automatically validate the nested structure.

4. What happens if validation fails?

FastAPI returns a 422 Unprocessable Entity response with a detailed error message indicating which fields failed validation and why.

5. Can I use Pydantic models with query parameters?

Yes, but you need to use Depends() with a Pydantic model to parse query parameters. However, for simple cases, individual query parameters are more straightforward.

Congratulations on completing Module 4! You now have a solid understanding of data validation in FastAPI. In Module 5, we will explore Dependency Injection—a powerful pattern for reusing code and managing shared logic across your API. Get ready to take your FastAPI skills to the next level!

Additional Practical Example

Let’s build a more comprehensive example that combines multiple concepts from this module: a user profile management system with nested models, validation, and response filtering.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, EmailStr, Field, validator
from typing import List, Optional
from datetime import datetime
import uuid

app = FastAPI(title="User Profile API")

# --- Address Model ---
class Address(BaseModel):
    street: str = Field(..., min_length=5, max_length=100)
    city: str = Field(..., min_length=2, max_length=50)
    zip_code: str = Field(..., regex=r"^d{5}(-d{4})?$")
    country: str = Field(default="USA", max_length=50)

# --- User Profile Model ---
class UserProfile(BaseModel):
    user_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    username: str = Field(..., min_length=3, max_length=20, regex=r"^[a-zA-Z0-9_]+$")
    email: EmailStr
    full_name: str = Field(..., min_length=2, max_length=100)
    age: int = Field(..., ge=18, le=120)
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.utcnow)
    addresses: List[Address] = Field(default_factory=list)

    @validator("username")
    def validate_username_no_admin(cls, v):
        if v.lower() == "admin":
            raise ValueError("Username cannot be 'admin'")
        return v

    @validator("age")
    def validate_age_range(cls, v):
        if v < 18:
            raise ValueError("User must be at least 18 years old")
        return v

# --- Response Models ---
class UserProfileResponse(BaseModel):
    user_id: str
    username: str
    full_name: str
    is_active: bool
    created_at: datetime
    addresses: List[Address]

class UserProfileSummary(BaseModel):
    user_id: str
    username: str
    is_active: bool

# --- In-memory database ---
fake_db = {}

# --- Endpoints ---
@app.post("/users/", response_model=UserProfileResponse, status_code=status.HTTP_201_CREATED)
async def create_user_profile(profile: UserProfile):
    """Create a new user profile with validation."""
    if profile.username in [u.username for u in fake_db.values()]:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username already exists"
        )
    
    fake_db[profile.user_id] = profile
    return profile

@app.get("/users/{user_id}", response_model=UserProfileResponse)
async def get_user_profile(user_id: str):
    """Retrieve a full user profile."""
    if user_id not in fake_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found"
        )
    return fake_db[user_id]

@app.get("/users/", response_model=List[UserProfileSummary])
async def list_users():
    """List all users with summary information."""
    return [
        UserProfileSummary(
            user_id=u.user_id,
            username=u.username,
            is_active=u.is_active
        )
        for u in fake_db.values()
    ]

@app.patch("/users/{user_id}/activate", response_model=UserProfileResponse)
async def activate_user(user_id: str):
    """Activate a user profile."""
    if user_id not in fake_db:
        raise HTTPException(status_code=404, detail="User not found")
    
    user = fake_db[user_id]
    user.is_active = True
    return user

This example demonstrates several important patterns:

  • Nested models: The Address model is embedded inside UserProfile using List[Address]
  • Custom validation: We use @validator decorators to check username uniqueness and age requirements
  • Field constraints: Field() with min_length, max_length, regex, and ge/le ensures data integrity
  • Multiple response models: We define UserProfileResponse (full data) and UserProfileSummary (minimal data) for different endpoints
  • Error handling: Proper HTTP exceptions with meaningful messages

Class-Based Implementation Example

While FastAPI works well with function-based views, you can also organize your code using classes. This is particularly useful for grouping related endpoints and reusing logic. Here’s how to implement the same user profile system using a class-based approach:

from fastapi import FastAPI, HTTPException, status, Depends
from pydantic import BaseModel, EmailStr, Field, validator
from typing import List, Optional
from datetime import datetime
import uuid

app = FastAPI(title="Class-Based User API")

# --- Models (same as before) ---
class Address(BaseModel):
    street: str = Field(..., min_length=5, max_length=100)
    city: str = Field(..., min_length=2, max_length=50)
    zip_code: str = Field(..., regex=r"^d{5}(-d{4})?$")
    country: str = Field(default="USA", max_length=50)

class UserProfile(BaseModel):
    user_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    username: str = Field(..., min_length=3, max_length=20, regex=r"^[a-zA-Z0-9_]+$")
    email: EmailStr
    full_name: str = Field(..., min_length=2, max_length=100)
    age: int = Field(..., ge=18, le=120)
    is_active: bool = True
    created_at: datetime = Field(default_factory=datetime.utcnow)
    addresses: List[Address] = Field(default_factory=list)

    @validator("username")
    def validate_username_no_admin(cls, v):
        if v.lower() == "admin":
            raise ValueError("Username cannot be 'admin'")
        return v

class UserProfileResponse(BaseModel):
    user_id: str
    username: str
    full_name: str
    is_active: bool
    created_at: datetime
    addresses: List[Address]

class UserProfileSummary(BaseModel):
    user_id: str
    username: str
    is_active: bool

# --- Database dependency ---
class Database:
    def __init__(self):
        self._users = {}
    
    def add_user(self, user: UserProfile) -> UserProfile:
        if user.username in [u.username for u in self._users.values()]:
            raise HTTPException(status_code=400, detail="Username exists")
        self._users[user.user_id] = user
        return user
    
    def get_user(self, user_id: str) -> UserProfile:
        if user_id not in self._users:
            raise HTTPException(status_code=404, detail="User not found")
        return self._users[user_id]
    
    def list_users(self) -> List[UserProfile]:
        return list(self._users.values())
    
    def activate_user(self, user_id: str) -> UserProfile:
        user = self.get_user(user_id)
        user.is_active = True
        return user

# --- Dependency injection ---
def get_database() -> Database:
    return Database()

# --- Class-based router ---
class UserRouter:
    def __init__(self, db: Database = Depends(get_database)):
        self.db = db
    
    async def create_user(self, profile: UserProfile) -> UserProfileResponse:
        return self.db.add_user(profile)
    
    async def get_user(self, user_id: str) -> UserProfileResponse:
        return self.db.get_user(user_id)
    
    async def list_users(self) -> List[UserProfileSummary]:
        users = self.db.list_users()
        return [
            UserProfileSummary(
                user_id=u.user_id,
                username=u.username,
                is_active=u.is_active
            )
            for u in users
        ]
    
    async def activate_user(self, user_id: str) -> UserProfileResponse:
        return self.db.activate_user(user_id)

# --- Register endpoints using class methods ---
user_router = UserRouter()

@app.post("/users/", response_model=UserProfileResponse, status_code=201)
async def create_user(profile: UserProfile):
    return await user_router.create_user(profile)

@app.get("/users/{user_id}", response_model=UserProfileResponse)
async def get_user(user_id: str):
    return await user_router.get_user(user_id)

@app.get("/users/", response_model=List[UserProfileSummary])
async def list_users():
    return await user_router.list_users()

@app.patch("/users/{user_id}/activate", response_model=UserProfileResponse)
async def activate_user(user_id: str):
    return await user_router.activate_user(user_id)

The class-based approach offers several advantages:

  • Code organization: Related methods are grouped together, making the codebase easier to navigate
  • Reusability: You can instantiate the router with different database instances for testing
  • Dependency injection: The Database class can be easily swapped with a real database connection
  • Separation of concerns: Business logic lives in the router class, while endpoint definitions remain clean

Hands-On Practice Task

Now it’s your turn to apply what you’ve learned. Build a book inventory management system with the following requirements:

  1. Create a Book model with fields:
    • isbn (string, must match pattern ^d{3}-d{10}$)
    • title (string, 1-200 characters)
    • author (string, 2-100 characters)
    • publication_year (integer, between 1900 and current year)
    • price (float, between 0.01 and 999.99)
    • genres (list of strings, each 3-30 characters)
    • in_stock (boolean, default True)
  2. Create response models:
    • BookResponse (all fields except internal notes)
    • BookSummary (only isbn, title, author, price, in_stock)
  3. Implement endpoints:
    • POST /books/ – Add a new book (return full details)
    • GET /books/{isbn} – Get a specific book
    • GET /books/ – List all books (return summaries only)
    • PATCH /books/{isbn}/stock – Toggle stock status
  4. Add validation:
    • Ensure ISBN is unique
    • Validate publication year is not in the future
    • Ensure at least one genre is provided

Starter code:

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import datetime

app = FastAPI(title="Book Inventory")

# Your code here
# 1. Define Book model with all fields and validators
# 2. Define BookResponse and BookSummary models
# 3. Create in-memory storage
# 4. Implement all endpoints

Expected output when testing:

# Test creating a book
curl -X POST "http://localhost:8000/books/" 
  -H "Content-Type: application/json" 
  -d '{
    "isbn": "978-1234567890",
    "title": "Python for Beginners",
    "author": "John Doe",
    "publication_year": 2023,
    "price": 29.99,
    "genres": ["Programming", "Education"]
  }'

# Expected response (201 Created):
{
  "isbn": "978-1234567890",
  "title": "Python for Beginners",
  "author": "John Doe",
  "publication_year": 2023,
  "price": 29.99,
  "genres": ["Programming", "Education"],
  "in_stock": true
}

# Test listing books
curl "http://localhost:8000/books/"

# Expected response (200 OK):
[
  {
    "isbn": "978-1234567890",
    "title": "Python for Beginners",
    "author": "John Doe",
    "price": 29.99,
    "in_stock": true
  }
]

Hints:

  • Use Field(default_factory=datetime.utcnow) for timestamps
  • Use @validator for custom validation logic
  • Store books in a dictionary keyed by ISBN
  • Use response_model parameter to control output

Common Interview Questions

Here are typical interview questions related to request and response models in FastAPI, along with detailed answers:

Q1: What is the difference between a Pydantic model and a FastAPI response model?

Answer: A Pydantic model defines the data structure, validation, and serialization logic. A FastAPI response model (specified via the response_model parameter) controls what data is actually sent to the client. The response model can be a subset or transformation of the internal model. For example, you might have a User model with a password_hash field, but your response model UserResponse excludes that field for security. FastAPI automatically filters the data based on the response model definition.

Q2: How do you handle optional fields in request bodies?

Answer: Use Optional[type] from Python’s typing module combined with a default value. For example:

from typing import Optional
from pydantic import BaseModel

class UpdateItem(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None

This allows clients to send partial updates. If a field is not provided, it defaults to None. You can then check which fields are None to implement partial updates.

Q3: Explain the purpose of response_model_exclude_unset.

Answer: When set to True, FastAPI will only include fields that were explicitly set by the client in the response. This is useful for PATCH endpoints where you want to return only the fields that were actually updated. For example:

@app.patch("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def update_item(item_id: int, item: Item):
    # Only return fields that were provided in the request
    return item

Q4: How do you validate complex nested data structures?

Answer: Pydantic supports nested models natively. You can define models within models:

class Address(BaseModel):
    street: str
    city: str

class User(BaseModel):
    name: str
    address: Address  # Nested model

class Company(BaseModel):
    name: str
    employees: List[User]  # List of nested models

FastAPI automatically validates all levels of nesting. You can also add @validator methods to nested models for custom validation at each level.

Q5: What is the difference between Field() and validator?

Answer: Field() is used to define constraints on a single field, such as minimum length, maximum value, regex patterns, and default values. Validators (@validator) are functions that can perform complex validation logic that may involve multiple fields or external data. For example, Field(min_length=3) ensures a string has at least 3 characters, while a validator can check that a password matches a confirmation field or that a username doesn’t exist in a database.

Q6: How do you handle file uploads with Pydantic models?

Answer: File uploads in FastAPI are handled separately from Pydantic models using UploadFile and Form parameters. You cannot directly include file data in a JSON request body. Instead, you use multipart form data:

from fastapi import FastAPI, UploadFile, File, Form
from pydantic import BaseModel

class ItemMetadata(BaseModel):
    name: str
    description: str

@app.post("/items/")
async def create_item(
    metadata: str = Form(...),  # JSON string of metadata
    file: UploadFile = File(...)
):
    item_metadata = ItemMetadata.parse_raw(metadata)
    # Process file and metadata

Q7: What happens if you don’t specify a response model?

Answer: If you don’t specify response_model, FastAPI will return the raw Pydantic model object, which gets serialized to JSON using the model’s default serialization. This means all fields will be included unless you’ve used Field(exclude=True) or other exclusion mechanisms. It’s generally good practice to always specify a response model to have explicit control over what data is exposed.

Q8: How can you make a field read-only in the response?

Answer: You can use different models for input and output. For example, the input model includes password, but the output model excludes it. Alternatively, you can use Pydantic’s Field(..., exclude=True) on the response model or use response_model_exclude parameter:

class UserInput(BaseModel):
    username: str
    password: str

class UserOutput(BaseModel):
    username: str
    # No password field

@app.post("/users/", response_model=UserOutput)
async def create_user(user: UserInput):
    # Process user, return only username
    return UserOutput(username=user.username)

These questions cover the most important concepts from this module. Understanding them will help you both in interviews and in building robust FastAPI applications.

Leave a Reply

Your email address will not be published. Required fields are marked *