Flask OpenAPI Documentation Package
A powerful, reusable package for generating comprehensive OpenAPI documentation with Flask-Pydantic-Spec. Features custom metadata, security schemes, and flexible configuration options.
✨ Features
- Flexible Configuration: Easy-to-use configuration system for API metadata
- Custom Security Schemes: Support for multiple authentication types
- Tag-based Organization: Group endpoints by functionality
- Route Format Control: Choose between Flask (
<string:param>) and OpenAPI ({param}) formats
- Automatic Schema Registration: Seamless Pydantic model integration
- Backward Compatibility: Drop-in replacement for existing setups
- Base Response Schemas: Automatic canonical response envelope wrapping with customizable base schemas
🚀 Quick Start
Basic Usage
from flask import Flask
from pydantic import BaseModel
from quas_docs import FlaskOpenAPISpec, DocsConfig, ContactInfo, SecurityScheme, endpoint
class UserData(BaseModel):
id: int
username: str
email: str
class ErrorData(BaseModel):
error_code: str
error_message: str
config = DocsConfig(
title="My API",
version="0.1.2",
description="A sample API with comprehensive documentation",
contact=ContactInfo(
email="api@example.com",
name="API Team"
)
)
spec = FlaskOpenAPISpec(config)
app = Flask(__name__)
@app.post("/users")
@endpoint(
request_body=CreateUserRequest,
responses={"200": UserData, "400": ErrorData},
security=SecurityScheme.BEARER_AUTH,
tags=["Users"],
summary="Create New User",
description="Creates a new user account with validation"
)
def create_user():
return {"message": "User created"}
spec.init_app(app)
Dynamic Configuration Methods
from quas_docs import FlaskOpenAPISpec, DocsConfig
config = DocsConfig.from_dict({
'title': 'My API',
'version': '2.0.0',
'description': 'API built with dynamic configuration',
'contact': {
'email': 'dev@mycompany.com',
'name': 'Development Team',
'url': 'https://mycompany.com'
},
'security_schemes': {
'BearerAuth': {'description': 'JWT authentication'},
'ApiKeyAuth': {
'scheme_type': 'apiKey',
'location': 'header',
'parameter_name': 'X-API-Key',
'description': 'API key authentication'
}
},
'preserve_flask_routes': True
})
config = DocsConfig.from_env(prefix="MY_API_")
config = DocsConfig.create_default()
config.title = "Custom API"
config.version = "2.0.0"
config = DocsConfig(
title="Custom API",
version="2.0.0",
description="Custom API with specific requirements"
)
📋 Configuration Options
DocsConfig Class
@dataclass
class DocsConfig:
title: str = "Flask API"
version: str = "0.1.2"
description: Optional[str] = None
terms_of_service: Optional[str] = None
contact: Optional[ContactInfo] = None
license_name: Optional[str] = None
license_url: Optional[str] = None
servers: List[Dict[str, str]] = field(default_factory=list)
security_schemes: Dict[str, SecuritySchemeConfig] = field(default_factory=dict)
preserve_flask_routes: bool = True
clear_auto_discovered: bool = True
add_default_responses: bool = True
custom_response_schemas: Dict[int, Type[BaseModel]] = field(default_factory=dict)
use_response_wrapper: bool = True
external_docs_url: Optional[str] = None
external_docs_description: Optional[str] = None
Security Schemes
from quas_docs import SecuritySchemeConfig
api_key_config = SecuritySchemeConfig(
name="ApiKeyAuth",
scheme_type="apiKey",
location="header",
parameter_name="X-API-Key",
description="API key authentication"
)
bearer_config = SecuritySchemeConfig(
name="BearerAuth",
scheme_type="apiKey",
location="header",
parameter_name="Authorization",
description="JWT Bearer token authentication"
)
config.add_security_scheme("ApiKeyAuth", api_key_config)
config.add_security_scheme("BearerAuth", bearer_config)
Contact Information
from quas_docs import ContactInfo
contact = ContactInfo(
email="api@example.com",
name="API Development Team",
url="https://example.com/contact"
)
config.contact = contact
🎯 Endpoint Decoration
The @endpoint Decorator
@endpoint(
request_body=Optional[Type[BaseModel]],
responses=Optional[Dict[str|int, Type[BaseModel]]],
security=Optional[SecurityScheme],
tags=Optional[List[str]],
summary=Optional[str],
description=Optional[str],
query_params=Optional[List[QueryParameter]],
deprecated=bool,
**extra_metadata
)
Examples
from pydantic import BaseModel
class LoginData(BaseModel):
access_token: str
user_id: str
class ErrorData(BaseModel):
error_code: str
error_message: str
@app.post("/auth/login")
@endpoint(
request_body=LoginRequest,
responses={"200": LoginData, "400": ErrorData, "401": ErrorData},
tags=["Authentication"],
summary="User Login",
description="Authenticate user with email and password"
)
def login():
return AuthController.login()
@app.get("/users")
@endpoint(
responses={"200": UsersListData},
security=SecurityScheme.BEARER_AUTH,
tags=["Users"],
summary="List Users",
description="Get paginated list of users with filtering options",
query_params=[
QueryParameter("page", "integer", required=False, description="Page number", default=1),
QueryParameter("per_page", "integer", required=False, description="Items per page", default=10),
QueryParameter("search", "string", required=False, description="Search by name or email"),
QueryParameter("active", "boolean", required=False, description="Filter by active status", default=True),
]
)
def list_users():
return UserController.list()
@app.get("/health")
@endpoint(
tags=["System"],
summary="Health Check",
description="Check API health status",
custom_field="health-check"
)
def health_check():
return {"status": "healthy"}
🔧 Advanced Configuration
Base Response Schemas
The package provides a canonical response envelope system that automatically wraps your endpoint data models in consistent base response schemas. This ensures all API responses follow a standard format with status, status_code, message, and data fields.
Pydantic v2 note: $defs emitted by Pydantic v2 schemas are automatically hoisted into components/schemas and $ref targets are rewritten, so Swagger/Redoc can resolve nested models without workarounds or v1 imports.
Default Base Response Schemas
The package includes default base response schemas for common HTTP status codes:
SuccessResp (200) - Standard success response
CreatedResp (201) - Resource created
NoContentResp (204) - No content
BadRequestResp (400) - Bad request
UnauthorizedResp (401) - Unauthorized
ForbiddenResp (403) - Forbidden
NotFoundResp (404) - Not found
ConflictResp (409) - Conflict
InternalServerErrorResp (500) - Server error
Using Default Base Response Schemas
Important: Response wrapping is enabled by default (use_response_wrapper=True). When you specify data models in the responses parameter, they are automatically wrapped in the appropriate base response schema based on the status code.
Simply specify your data models in the responses parameter:
from quas_docs import FlaskOpenAPISpec, DocsConfig, endpoint
from pydantic import BaseModel
class LoginData(BaseModel):
access_token: str
user_id: str
access_data: dict
class ErrorData(BaseModel):
error_code: str
error_message: str
config = DocsConfig(
title="My API",
version="0.1.2",
use_response_wrapper=True
)
spec = FlaskOpenAPISpec(config)
@app.post("/login")
@endpoint(
request_body=LoginRequest,
responses={"200": LoginData, "400": ErrorData, "401": ErrorData},
tags=["Authentication"]
)
def login():
return {...}
This will generate response schemas where:
200 responses wrap LoginData in SuccessResp with LoginData as the data field
400 and 401 responses wrap ErrorData in BadRequestResp and UnauthorizedResp respectively
The resulting response structure:
{
"status": "success",
"status_code": 200,
"message": "string",
"data": {
"access_token": "",
"user_id": "",
"access_data": {}
}
}
Custom Base Response Schemas
You can define your own base response schemas that override the defaults for specific status codes. Important: Custom schemas must have a data field in their schema definition, but they don't need to inherit from the default schemas - they can be completely independent Pydantic models.
from pydantic import BaseModel
from quas_docs import DocsConfig
class MyCustomSuccessResp(BaseModel):
status: str = "success"
status_code: int = 200
message: str
data: dict
class MyCustomErrorResp(BaseModel):
status: str = "error"
code: int = 400
message: str
errors: list = []
data: dict
config = DocsConfig(title="My API", version="0.1.2")
config.add_custom_response_schema(200, MyCustomSuccessResp)
config.add_custom_response_schema(400, MyCustomErrorResp)
@app.post("/login")
@endpoint(
responses={"200": LoginData, "400": ErrorData, "401": ErrorData}
)
def login():
...
Mixed Usage: If you define custom schemas for some status codes but not others, the package automatically falls back to defaults for undefined status codes. For example, if you define custom schemas for 200 and 400, but an endpoint uses 401, the default UnauthorizedResp will be used.
Disabling Response Wrapping
To disable response wrapping globally:
config = DocsConfig(
title="My API",
version="0.1.2",
use_response_wrapper=False
)
When disabled, data models are used directly without base response envelopes.
Modeling nested responses (avoiding additionalProp1)
additionalProp1 (or additionalProperties) shows up when a field is a free-form dict/Dict[str, Any]. Swagger can't infer keys, so it renders placeholders.
- Prefer typed nested models for clearer docs. Example:
class Product(BaseModel):
id: int
name: str
price: float
currency: str
class ProductListData(BaseModel):
products: list[Product]
total: int
page: int
per_page: int
total_pages: int
- If you truly need arbitrary dicts, add examples to make the docs usable:
class CheckoutData(BaseModel):
order: dict[str, Any] = Field(
...,
examples=[{"id": 123, "status": "paid", "meta": {"channel": "web"}}],
description="Free-form order data",
)
- Example with nested models and examples:
class OrderItem(BaseModel):
sku: str
quantity: int
price: float
class Order(BaseModel):
id: int
total: float
currency: str
items: list[OrderItem]
class CheckoutData(BaseModel):
order: Order = Field(..., examples=[{
"id": 123,
"total": 199.99,
"currency": "NGN",
"items": [{"sku": "ABC123", "quantity": 2, "price": 99.99}],
}])
Using @spec.validate (Legacy/Backward Compatibility)
The @spec.validate decorator from flask-pydantic-spec is still supported for backward compatibility, but using the responses parameter in @endpoint is the recommended approach:
@app.post("/users")
@endpoint(
tags=["Users"],
responses={"201": UserData, "400": ErrorData, "409": ErrorData}
)
def create_user():
return UserController.create()
@app.post("/users")
@endpoint(tags=["Users"])
@spec.validate(resp=Response(
HTTP_201=CreateUserResponse,
HTTP_400=ErrorResponse,
HTTP_409=ConflictResponse
))
def create_user():
return UserController.create()
config.add_default_responses = False
Route Format Control
config.preserve_flask_routes = True
config.preserve_flask_routes = False
Servers Configuration
config.add_server("https://api.example.com", "Production")
config.add_server("https://staging-api.example.com", "Staging")
config.add_server("http://localhost:5000", "Development")
External Documentation
config.external_docs_url = "https://docs.example.com"
config.external_docs_description = "Complete API Documentation"
Environment Variable Configuration
Set environment variables and load them automatically:
export API_TITLE="My Project API"
export API_VERSION="1.2.0"
export API_DESCRIPTION="API for my awesome project"
export API_CONTACT_EMAIL="api@myproject.com"
export API_CONTACT_NAME="API Team"
export API_CONTACT_URL="https://myproject.com/contact"
export API_LICENSE_NAME="MIT"
export API_PRESERVE_FLASK_ROUTES="true"
config = DocsConfig.from_env()
config = DocsConfig.from_env(prefix="MYAPI_")
📦 Integration Examples
Replace Existing Setup
If you have an existing Flask app with manual OpenAPI setup:
from flask_pydantic_spec import FlaskPydanticSpec
spec = FlaskPydanticSpec('flask', title='My API', version='0.1.2')
from quas_docs import FlaskOpenAPISpec, DocsConfig
config = DocsConfig(title='My API', version='0.1.2')
spec_instance = FlaskOpenAPISpec(config)
spec = spec_instance.spec
Multiple APIs
public_config = DocsConfig(
title="Public API",
version="0.1.2",
preserve_flask_routes=True
)
public_spec = FlaskOpenAPISpec(public_config)
admin_config = DocsConfig(
title="Admin API",
version="0.1.2",
preserve_flask_routes=False
)
admin_spec = FlaskOpenAPISpec(admin_config)
Custom Project Setup
from quas_docs import DocsConfig, ContactInfo, SecuritySchemeConfig
def create_my_project_config():
config = DocsConfig(
title="My Project API",
version="2.1.0",
description="Custom project with specific requirements",
contact=ContactInfo(
email="dev@myproject.com",
name="Development Team",
url="https://myproject.com"
)
)
config.add_security_scheme("ApiKey", SecuritySchemeConfig(
name="ApiKey",
parameter_name="X-API-Key",
description="Project-specific API key"
))
config.add_server("https://api.myproject.com", "Production")
config.add_server("http://localhost:8000", "Development")
return config
from quas_docs import FlaskOpenAPISpec
from .docs_config import create_my_project_config
config = create_my_project_config()
spec = FlaskOpenAPISpec(config)
🔄 Migration Guide
From Manual Setup
- Copy the docs/ folder to your project
- Replace your existing docs setup:
from quas_docs import FlaskOpenAPISpec, DocsConfig, endpoint, SecurityScheme
config = DocsConfig.from_dict({
'title': 'Your API Name',
'version': '0.1.2',
})
spec_instance = FlaskOpenAPISpec(config)
spec = spec_instance.spec
- Use the @endpoint decorator:
@endpoint(
request_body=YourModel,
security=SecurityScheme.BEARER_AUTH,
tags=["Your Tag"],
summary="Your Summary"
)
- Initialize documentation:
spec_instance.init_app(app)
Clean v1.0 Design
The package follows a clean, modern approach:
- Single
@endpoint decorator for all metadata
- Configuration-driven setup
- No legacy compatibility code
- Streamlined API surface
📝 Best Practices
- Use meaningful tags to organize endpoints logically
- Provide clear summaries and descriptions for better developer experience
- Configure contact information for API support
- Use appropriate security schemes for different endpoint types
- Test documentation in both Swagger UI and Redoc
- Version your APIs properly using semantic versioning
🐛 Troubleshooting
Common Issues
Issue: Endpoints appear in "default" category
Solution: Ensure clear_auto_discovered = True in config
Issue: Route parameters show wrong format
Solution: Set preserve_flask_routes in config
Issue: Security schemes not working
Solution: Verify security scheme names match those in config
Issue: Missing response schemas
Solution: Check add_default_responses setting. Use the responses parameter in @endpoint decorator to specify response data models.
Issue: Response wrapping not working
Solution: Ensure use_response_wrapper=True in config (enabled by default). Data models specified in responses are automatically wrapped in base response schemas.
Issue: Custom base response schema validation error
Solution: Ensure your custom schema has a data field. Use config.add_custom_response_schema(status_code, schema) to register custom schemas.
📄 License
MIT License - feel free to use in your projects!
🤝 Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
Created by Emmanuel Olowu | GitHub | Website