FastAPI JSON Response
Introduction
When building web APIs, you'll frequently need to return data to clients in a structured format. JSON (JavaScript Object Notation) has become the de facto standard for data exchange in web applications. FastAPI, being a modern API framework, provides excellent support for JSON responses through its JSONResponse
class and automatic serialization.
In this tutorial, you'll learn how to:
- Return JSON responses using FastAPI
- Customize JSON responses with status codes and headers
- Handle different data types in JSON responses
- Use Pydantic models with JSON responses
- Implement advanced JSON response patterns
JSON Response Basics
FastAPI automatically converts Python dictionaries, lists, and Pydantic models to JSON responses. Let's start with the basics:
Automatic JSON Conversion
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items():
return {"message": "Hello World", "items": [1, 2, 3, 4, 5]}
When you access the /items/
endpoint, FastAPI will:
- Take the dictionary you returned
- Convert it to JSON
- Set the
Content-Type
header toapplication/json
- Send the JSON response to the client
The response would look like:
{
"message": "Hello World",
"items": [1, 2, 3, 4, 5]
}
Using the JSONResponse Explicitly
While FastAPI handles JSON conversion automatically, sometimes you need more control. That's where the JSONResponse
class comes in:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/custom-response/")
async def get_custom_response():
content = {
"message": "This is a custom JSON response",
"status": "success"
}
return JSONResponse(content=content, status_code=200)
Setting Custom Status Codes and Headers
You can customize the status code and headers:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/not-found/")
async def read_not_found():
content = {"error": "Resource not found"}
headers = {"X-Error-Code": "RESOURCE_NOT_FOUND"}
return JSONResponse(
content=content,
status_code=404,
headers=headers
)
Working with Pydantic Models
Pydantic models work seamlessly with FastAPI's JSON response handling:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Item(BaseModel):
id: int
name: str
description: Optional[str] = None
price: float
tags: List[str] = []
@app.get("/items/{item_id}")
async def read_item(item_id: int):
# In a real app, you would fetch from a database
item = Item(
id=item_id,
name="Example Item",
description="This is an example item",
price=45.5,
tags=["example", "item"]
)
return item # FastAPI automatically converts the Pydantic model to JSON
The response will be:
{
"id": 1,
"name": "Example Item",
"description": "This is an example item",
"price": 45.5,
"tags": ["example", "item"]
}
Handling Different Data Types
FastAPI handles various Python data types in JSON responses:
Lists and Nested Objects
from fastapi import FastAPI
app = FastAPI()
@app.get("/complex-data/")
async def get_complex_data():
return {
"string_value": "hello",
"integer_value": 42,
"float_value": 3.14,
"boolean_value": True,
"null_value": None,
"array": [1, 2, 3, 4, 5],
"nested_object": {
"name": "John",
"age": 30,
"address": {
"street": "123 Main St",
"city": "New York"
}
},
"mixed_array": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
]
}
Date and Time Objects
from fastapi import FastAPI
from datetime import datetime, date
app = FastAPI()
@app.get("/datetime-example/")
async def get_datetime_example():
return {
"current_datetime": datetime.now(),
"current_date": date.today(),
"specific_date": datetime(2023, 1, 1, 12, 0, 0)
}
FastAPI's JSON encoder will convert datetime objects to ISO format strings automatically.
Real-World Example: Building an API with JSON Responses
Let's build a simple API for a book repository:
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
from typing import List, Optional
from fastapi.responses import JSONResponse
app = FastAPI()
# Define our data model
class Book(BaseModel):
id: int
title: str
author: str
year: int
genre: Optional[str] = None
# Sample data (in a real app, you'd use a database)
books_db = [
Book(id=1, title="To Kill a Mockingbird", author="Harper Lee", year=1960, genre="Fiction"),
Book(id=2, title="1984", author="George Orwell", year=1949, genre="Dystopian"),
Book(id=3, title="The Great Gatsby", author="F. Scott Fitzgerald", year=1925, genre="Fiction"),
]
@app.get("/books/", response_model=List[Book])
async def get_all_books(
genre: Optional[str] = Query(None, description="Filter books by genre")
):
if genre:
filtered_books = [book for book in books_db if book.genre == genre]
return filtered_books
return books_db
@app.get("/books/{book_id}", response_model=Book)
async def get_book(book_id: int):
for book in books_db:
if book.id == book_id:
return book
# Custom error response
error_content = {
"error": "Book not found",
"details": f"Book with ID {book_id} does not exist"
}
return JSONResponse(status_code=404, content=error_content)
@app.post("/books/", response_model=Book, status_code=201)
async def create_book(book: Book):
# In a real app, you would save to a database
# Check if book with same ID exists
if any(b.id == book.id for b in books_db):
error_content = {
"error": "Duplicate ID",
"details": f"Book with ID {book.id} already exists"
}
return JSONResponse(status_code=400, content=error_content)
books_db.append(book)
return book
Advanced Techniques
Custom JSON Encoding
Sometimes you need to handle custom objects that aren't natively serializable to JSON:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import json
from typing import Any
class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj: Any) -> Any:
if isinstance(obj, set):
return list(obj) # Convert sets to lists
# Add other custom type conversions here
return super().default(obj)
app = FastAPI()
@app.get("/custom-types/")
async def get_custom_types():
data = {
"my_set": {1, 2, 3, 4}, # Sets aren't JSON serializable by default
"regular_list": [5, 6, 7]
}
# Use custom JSON encoder
json_data = json.dumps(data, cls=CustomJSONEncoder)
return JSONResponse(content=json.loads(json_data))
Streaming JSON Responses
For large datasets, you might want to stream the response:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json
app = FastAPI()
@app.get("/stream-data/")
async def stream_data():
async def generate_large_data():
# Yield the opening bracket
yield "{"
# Stream items one by one
for i in range(10000):
prefix = "" if i == 0 else ","
yield f'{prefix}"item_{i}": {i}'
# Yield the closing bracket
yield "}"
return StreamingResponse(
generate_large_data(),
media_type="application/json"
)
Error Handling with JSON Responses
FastAPI allows you to create custom exception handlers that return JSON:
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class CustomException(Exception):
def __init__(self, name: str, code: str, message: str):
self.name = name
self.code = code
self.message = message
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
return JSONResponse(
status_code=400,
content={
"error": exc.name,
"code": exc.code,
"message": exc.message,
"path": request.url.path
}
)
@app.get("/trigger-error/")
async def trigger_error():
raise CustomException(
name="ValidationError",
code="USER_ID_INVALID",
message="The user ID provided is not valid"
)
Summary
FastAPI provides robust support for JSON responses:
- Automatic conversion: FastAPI automatically converts Python dictionaries, lists, and Pydantic models to JSON
- Explicit responses: Use
JSONResponse
when you need more control over status codes and headers - Pydantic integration: Pydantic models are automatically serialized to JSON
- Data type handling: FastAPI handles various Python data types, including datetime objects
- Custom encoding: You can implement custom JSON encoders for special types
- Error handling: Use custom exception handlers to return formatted JSON error responses
JSON responses are a fundamental part of building RESTful APIs with FastAPI. With the techniques covered in this tutorial, you have the knowledge to create standardized, well-structured APIs that communicate effectively with clients using JSON.
Additional Resources
- FastAPI Official Documentation on Responses
- Python's built-in JSON module
- Pydantic Documentation
- JSON Standard Specification
Exercises
- Create a FastAPI endpoint that returns a nested JSON structure with at least three levels of nesting.
- Implement an endpoint that returns a list of Pydantic models with at least 5 fields each.
- Create a custom exception handler for
ValueError
that returns appropriate JSON error responses. - Build a simple REST API with CRUD operations (Create, Read, Update, Delete) for a resource of your choice, using appropriate status codes and JSON responses.
- Implement a custom JSON encoder that can handle custom Python classes by converting them to dictionaries.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)