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:
- Validate incoming request data
- Generate automatic API documentation
- 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:
{
"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:
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:
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:
Constraint | Description | Example |
---|---|---|
min_length | Minimum string length | min_length=5 |
max_length | Maximum string length | max_length=50 |
regex | Regular expression pattern | regex="^[a-zA-Z0-9_]+$" |
gt , ge | Greater than, greater than or equal | gt=0 , ge=18 |
lt , le | Less than, less than or equal | lt=100 , le=65 |
default | Default value | default="example" |
example | Example 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:
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
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
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
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:
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:
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:
- Providing validation for incoming request data
- Generating documentation that is always in sync with your code
- 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
- FastAPI Official Documentation on JSON Schema
- JSON Schema Official Documentation
- Pydantic Field Documentation
Exercises
- Create a Pydantic model for a user registration form with proper validation rules and custom JSON Schema documentation.
- Implement an API endpoint that accepts a complex nested object with arrays and validate it using Pydantic models.
- Generate and examine the JSON Schema for a model with custom validators and see how they affect the schema.
- 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! :)