Skip to main content

FastAPI JSON Schema

Introduction

JSON Schema is a powerful tool for describing and validating the structure of JSON data. In FastAPI, JSON Schema is used behind the scenes to:

  1. Validate incoming request data
  2. Generate automatic API documentation
  3. Define the structure of responses

Pydantic models in FastAPI are automatically converted to JSON Schema, which enables FastAPI to provide automatic validation and documentation. Understanding how this conversion works gives you more control over your API's behavior and documentation.

What is JSON Schema?

JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It's a specification that defines how to describe the structure and validation requirements of your JSON data.

For example, a simple JSON Schema might look like this:

json
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 },
"is_active": { "type": "boolean" }
},
"required": ["name", "age"]
}

This schema describes an object with three properties: name (a required string), age (a required integer that must be at least 0), and is_active (an optional boolean).

How FastAPI Uses JSON Schema

When you define Pydantic models in FastAPI, they are automatically converted to JSON Schema definitions. Let's see how this works:

python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class User(BaseModel):
name: str = Field(..., description="The user's name")
age: int = Field(..., gt=0, description="The user's age in years")
is_active: bool = Field(default=True, description="Whether the user is active")

@app.post("/users/")
async def create_user(user: User):
return user

When FastAPI generates the OpenAPI documentation for this endpoint, it includes the JSON Schema derived from the User model. You can see this by navigating to the /docs or /openapi.json endpoints of your running application.

Customizing JSON Schema in Pydantic Models

Pydantic provides several ways to customize the JSON Schema generated from your models:

Using Field to Add Metadata

The Field function lets you add validation and metadata:

python
from pydantic import BaseModel, Field

class Product(BaseModel):
name: str = Field(
..., # ... means the field is required
min_length=1,
max_length=50,
description="The name of the product"
)
price: float = Field(
...,
gt=0,
description="The price of the product in USD"
)
tags: list[str] = Field(
default_factory=list,
description="Tags associated with the product"
)
in_stock: bool = Field(
default=True,
description="Whether the product is in stock"
)

Field Types and Constraints

Here are common validation constraints you can use with Pydantic fields:

ConstraintDescriptionExample
min_lengthMinimum string lengthmin_length=5
max_lengthMaximum string lengthmax_length=50
regexRegular expression patternregex="^[a-zA-Z0-9_]+$"
gt, geGreater than, greater than or equalgt=0, ge=18
lt, leLess than, less than or equallt=100, le=65
defaultDefault valuedefault="example"
exampleExample value (for docs)example="John Doe"

Schema Extra and Config

For more complex schema customization, you can use the schema_extra in the model's Config class:

python
class User(BaseModel):
name: str
age: int
bio: str | None = None

class Config:
schema_extra = {
"examples": [
{
"name": "John Doe",
"age": 30,
"bio": "A software developer from New York"
}
]
}

Viewing the Generated JSON Schema

You can view the JSON Schema that FastAPI generates for your models in several ways:

Using the model_schema method

python
from pydantic import BaseModel

class User(BaseModel):
name: str
age: int
is_active: bool = True

schema = User.model_schema()
print(schema)

This will output something like:

{
'title': 'User',
'type': 'object',
'properties': {
'name': {'title': 'Name', 'type': 'string'},
'age': {'title': 'Age', 'type': 'integer'},
'is_active': {'title': 'Is Active', 'default': True, 'type': 'boolean'}
},
'required': ['name', 'age']
}

In FastAPI Documentation

FastAPI automatically generates interactive documentation from your JSON Schema. When you run your FastAPI application, you can access the Swagger UI documentation at the /docs endpoint, which displays your models and their validation rules.

Practical Examples

1. API for a Blog Post with Validation

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

app = FastAPI()

class Comment(BaseModel):
id: int
content: str = Field(..., min_length=1, max_length=500)
created_at: datetime = Field(default_factory=datetime.now)

class Config:
schema_extra = {
"example": {
"id": 1,
"content": "Great post!",
"created_at": "2023-03-15T14:30:00Z"
}
}

class BlogPost(BaseModel):
title: str = Field(..., min_length=3, max_length=100)
content: str = Field(..., min_length=10)
tags: List[str] = Field(default_factory=list)
published: bool = True
comments: List[Comment] = Field(default_factory=list)

@validator('tags')
def tags_must_be_lowercase(cls, v):
return [tag.lower() for tag in v]

@app.post("/posts/", response_model=BlogPost)
async def create_post(post: BlogPost):
# Here you would typically save the post to a database
return post

@app.get("/sample-schema/")
async def get_post_schema():
return BlogPost.model_schema()

2. E-commerce Product API with Complex Validation

python
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field, validator, HttpUrl
from typing import List, Optional
from enum import Enum
from decimal import Decimal

app = FastAPI()

class CategoryEnum(str, Enum):
ELECTRONICS = "electronics"
CLOTHING = "clothing"
BOOKS = "books"
HOME = "home"
OTHER = "other"

class ProductImage(BaseModel):
url: HttpUrl
alt: str = Field(default="Product image")

class ProductVariant(BaseModel):
name: str
price: Decimal = Field(..., gt=0, lt=10000)
stock: int = Field(..., ge=0)

@validator('price')
def round_price(cls, v):
return round(v, 2)

class Product(BaseModel):
id: Optional[int] = None
name: str = Field(..., min_length=3, max_length=100)
description: str = Field(..., min_length=10, max_length=1000)
price: Decimal = Field(..., gt=0, lt=10000)
category: CategoryEnum
tags: List[str] = Field(default_factory=list)
images: List[ProductImage] = Field(default_factory=list)
variants: List[ProductVariant] = Field(default_factory=list)

class Config:
schema_extra = {
"example": {
"name": "Smartphone XYZ",
"description": "Latest smartphone with amazing features",
"price": 599.99,
"category": "electronics",
"tags": ["smartphone", "technology", "gadget"],
"images": [
{"url": "https://example.com/img1.jpg", "alt": "Front view"}
],
"variants": [
{"name": "Black", "price": 599.99, "stock": 10},
{"name": "White", "price": 599.99, "stock": 5}
]
}
}

@app.post("/products/", response_model=Product)
async def create_product(product: Product):
# Save product to database
return product

@app.get("/products/", response_model=List[Product])
async def list_products(
category: Optional[CategoryEnum] = None,
min_price: Optional[float] = Query(None, gt=0),
max_price: Optional[float] = Query(None, gt=0),
in_stock: bool = True
):
# Filter products based on parameters
# This would typically query a database
return []

Advanced JSON Schema Customization

Custom Field Types and Validators

You can create custom field types with specific JSON Schema representations:

python
from pydantic import BaseModel, Field, validator
import re

class UserProfile(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: str
phone: str | None = None

@validator('username')
def username_alphanumeric(cls, v):
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('Username must be alphanumeric')
return v

@validator('email')
def email_validate(cls, v):
if not re.match(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', v):
raise ValueError('Invalid email format')
return v

@validator('phone')
def phone_validate(cls, v):
if v is not None and not re.match(r'^\+?[1-9]\d{1,14}$', v):
raise ValueError('Invalid phone number format')
return v

class Config:
# This adds examples to the Swagger UI
schema_extra = {
"examples": [
{
"username": "john_doe",
"email": "john.doe@example.com",
"phone": "+12345678901"
}
]
}

Schema Customization with JSON Schema Extra

For even more complex schema customization:

python
from pydantic import BaseModel, Field
from typing import Dict, Any, List

class SensorData(BaseModel):
device_id: str
temperature: float = Field(..., description="Temperature in Celsius")
humidity: float = Field(..., description="Humidity percentage")
pressure: float = Field(..., description="Atmospheric pressure in hPa")
timestamp: int = Field(..., description="Unix timestamp")

class Config:
@staticmethod
def schema_extra(schema: Dict[str, Any], model: type["SensorData"]) -> None:
# Modify the generated schema
for prop in schema.get("properties", {}).values():
if "type" in prop and prop["type"] == "number":
# Add format to number fields
prop["format"] = "float"

# Add additional metadata
schema["externalDocs"] = {
"description": "Sensor data documentation",
"url": "https://example.com/docs/sensors"
}

Summary

JSON Schema plays a critical role in FastAPI applications by:

  1. Providing validation for incoming request data
  2. Generating documentation that is always in sync with your code
  3. Enabling automatic serialization and deserialization of data

Using Pydantic models with FastAPI gives you a powerful way to define and validate your data structures. The JSON Schema generated from these models allows FastAPI to provide automatic validation and comprehensive documentation, making your APIs more robust and easier to use.

By leveraging the various Pydantic features like Field(), validators, and the Config class, you can customize how your data models are represented in the JSON Schema and how they appear in the automatically generated documentation.

Additional Resources

Exercises

  1. Create a Pydantic model for a user registration form with proper validation rules and custom JSON Schema documentation.
  2. Implement an API endpoint that accepts a complex nested object with arrays and validate it using Pydantic models.
  3. Generate and examine the JSON Schema for a model with custom validators and see how they affect the schema.
  4. Create an API that allows filtering based on multiple query parameters and document them properly in the JSON Schema.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)