Skip to main content

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

python
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:

  1. Take the dictionary you returned
  2. Convert it to JSON
  3. Set the Content-Type header to application/json
  4. Send the JSON response to the client

The response would look like:

json
{
"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:

python
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:

python
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:

python
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:

json
{
"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

python
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

python
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:

python
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:

python
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:

python
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:

python
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

Exercises

  1. Create a FastAPI endpoint that returns a nested JSON structure with at least three levels of nesting.
  2. Implement an endpoint that returns a list of Pydantic models with at least 5 fields each.
  3. Create a custom exception handler for ValueError that returns appropriate JSON error responses.
  4. 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.
  5. 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! :)