Generating comprehensive and accurate API documentation is a critical but often tedious part of software development. The OpenAPI Specification (formerly known as Swagger) has emerged as the industry standard for defining RESTful APIs. It provides a machine-readable format that allows both humans and computers to discover and understand the capabilities of a service without access to source code, documentation, or through network traffic inspection.1
While many frameworks offer plugins to generate OpenAPI specs from code annotations (like docstrings), there are scenarios where you might need more direct, programmatic control over the specification's creation. This could be because you're working with a legacy system, a non-standard framework, or you need to generate a spec for an API that is composed of multiple microservices.
This is where pyswagger
comes in. It is a powerful Python library that functions as a toolkit for OpenAPI. While it's often used as an API client to consume services defined by an OpenAPI spec, its true power lies in its object model, which allows you to construct, manipulate, and validate a specification programmatically.
In this comprehensive tutorial, we will walk through the process of using pyswagger
to manually, yet automatically, generate a complete OpenAPI 3.0 specification for a simple Python web application built with Flask. We will build the specification from the ground up, piece by piece, demonstrating how pyswagger
's objects map directly to the components of the OpenAPI standard. By the end, you will not only have a generated openapi.json
file but also a live, interactive documentation UI served directly from your application.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demands, and replaces Postman at a much more affordable price!
Part 1: Setting Up the Project Environment
Before we can start generating our specification, we need to set up a proper development environment. This involves creating an isolated Python environment to manage our dependencies and installing the necessary libraries.
Creating Your Workspace ⚙️
First, let's create a directory for our project. Open your terminal or command prompt and run the following commands:Bash
# Create a new directory for our project
mkdir pyswagger-tutorial
cd pyswagger-tutorial
# Create a Python virtual environment
# On macOS/Linux
python3 -m venv venv
# On Windows
python -m venv venv
A virtual environment is a self-contained directory tree that includes a Python installation and a number of supporting files. Using a virtual environment ensures that the packages we install for this project don't conflict with packages installed for other projects.
Now, activate the virtual environment:Bash
# On macOS/Linux
source venv/bin/activate
# On Windows
.\venv\Scripts\activate
Once activated, your terminal prompt should change to show the name of the virtual environment (e.g., (venv)
), indicating that you are now working inside it.
Installing Necessary Libraries
With our environment active, we can install the Python libraries we'll need for this tutorial. We need pyswagger
itself to build the spec, Flask
to create our simple web API, and PyYAML
because pyswagger
uses it for YAML operations.Bash
pip install "pyswagger[utils]" Flask PyYAML
The [utils]
part when installing pyswagger
is a good practice as it includes helpful utilities, like a validator that we will use later to check our generated specification for correctness.
For good project management, it's wise to pin your dependencies in a requirements.txt
file.Bash
pip freeze > requirements.txt
Your requirements.txt
will now contain the libraries and their specific versions, making your project easily reproducible by others.
Creating the Basic Flask Application
Now, let's create a minimal Flask application. This will serve as the foundation of the API that we are going to document.
In your project directory, create a new file named app.py
and add the following code:Python
# app.py
from flask import Flask, jsonify
# Initialize the Flask application
app = Flask(__name__)
@app.route("/")
def index():
""" A simple endpoint to check if the app is running. """
return jsonify({"message": "API is up and running!"})
if __name__ == "__main__":
# Runs the Flask app on http://127.0.0.1:5000
app.run(debug=True)
This code sets up a very simple web server with a single endpoint. To run it, make sure your virtual environment is still active and execute the following command in your terminal:Bash
python app.py
You should see output indicating that the server is running, something like this:
* Serving Flask app 'app'
* Debug mode: on
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
You can now open your web browser or use a tool like curl
to visit http://127.0.0.1:5000
. You should see the JSON response: {"message": "API is up and running!"}
.
With our basic environment and application skeleton in place, we can now dive into the core concepts of pyswagger
.
Part 2: Understanding the pyswagger
Object Model
To effectively use pyswagger
to generate a specification, you first need to understand how its object model corresponds to the structure of an OpenAPI document. An OpenAPI specification is essentially a large JSON or YAML object with a specific schema. pyswagger
provides Python classes and objects that mirror this schema, allowing you to build the spec in a more intuitive, object-oriented way.
Core Concepts of the OpenAPI 3.0 Specification 📜
An OpenAPI 3.0 document has a few key top-level fields:
openapi
: A string specifying the OpenAPI Specification version (e.g.,'3.0.0'
).info
: An object providing metadata about the API. This includes thetitle
,version
,description
, and contact information.servers
: An array of server objects, which define the base URLs for the API.paths
: The most important field. This object holds all the available API endpoints (paths) and the HTTP operations (GET, POST, PUT, DELETE, etc.) that can be performed on them.components
: An object that holds a set of reusable objects for different parts of the spec. This is key for keeping your specification DRY (Don't Repeat Yourself). You can define reusableschemas
(data models),responses
,parameters
,examples
, and more.
Mapping OpenAPI to pyswagger
Objects
pyswagger
provides a clean mapping from these OpenAPI concepts to Python objects. Let's explore the primary ones we'll be using.
The central object in pyswagger
is the App
. You can think of an App
instance as the root of your OpenAPI document.Python
from pyswagger import App
# The root of the specification document
# We initialize it with a version, but it can also load from a URL or file
root_app = App(version='3.0.0')
Once you have your App
object, you can start populating its attributes, which directly correspond to the OpenAPI fields. pyswagger
uses a builder pattern, allowing for a fluent and readable syntax.
Info and Servers
The info
and servers
sections are straightforward to populate.Python
# Populating the 'info' object
root_app.info.title = "User API"
root_app.info.version = "1.0.0"
root_app.info.description = "A simple API to manage users, used for the pyswagger tutorial."
# Populating the 'servers' array
# You create a Server object and append it
server = root_app.prepare_obj('Server', {'url': 'http://127.0.0.1:5000', 'description': 'Local development server'})
root_app.servers.append(server)
Paths and Operations
Paths are defined on the App
object. You add new paths and then define the operations (HTTP methods) within them. Each operation is configured with details like a summary
, description
, parameters
, a requestBody
, and responses
.Python
# Defining a path and an operation
# This doesn't execute anything; it just builds the object structure.
path_item = root_app.define_path('/users')
get_op = path_item.define_op('get')
get_op.summary = "Retrieve a list of all users"
Components: Schemas, Parameters, and Responses
The real power of a well-structured OpenAPI spec comes from reusable components. Instead of defining the structure of a "User" object every time it appears in a response, you define it once in components/schemas
and then reference it using a $ref
pointer. pyswagger
handles this elegantly.
Schema
: A Schema
object defines a data model. You can specify its type (object
, string
, integer
) and its properties.Python
# Preparing a Schema object for a User
user_schema = root_app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'id': {'type': 'integer', 'format': 'int64'},
'username': {'type': 'string'},
'email': {'type': 'string', 'format': 'email'}
},
'required': ['id', 'username', 'email']
})
# Add it to the reusable components
root_app.components.schemas['User'] = user_schema
Parameter
: A Parameter
object defines a single operation parameter. You specify its name, where it is located (in
: 'path'
, 'query'
, 'header'
, or 'cookie'
), and its schema.Python
# Preparing a Parameter object for a user ID in the path
user_id_param = root_app.prepare_obj('Parameter', {
'name': 'user_id',
'in': 'path',
'description': 'ID of the user to retrieve',
'required': True,
'schema': {'type': 'integer'}
})
Response
: A Response
object defines the structure of a response for a specific HTTP status code. It includes a description
and the content
, which specifies the media type (e.g., application/json
) and its schema.Python
# Preparing a Response object for a 200 OK response returning a single user
# Note the use of '$ref' to point to our reusable User schema
ok_user_response = root_app.prepare_obj('Response', {
'description': 'Successful retrieval of a user',
'content': {
'application/json': {
'schema': {'$ref': '#/components/schemas/User'}
}
}
})
Understanding this mapping is the key to building your specification. You are essentially constructing a Python object graph that pyswagger
will later serialize into a valid OpenAPI JSON or YAML file.
Part 3: Building a Simple API with Flask
To make our documentation exercise practical, we need an actual API to document. We'll expand our simple Flask app from Part 1 into a minimal REST API for managing a list of users. This API will serve as the "source of truth" that we will describe with pyswagger
.
Designing a Simple "User" API 📝
We will implement four basic endpoints that represent common CRUD (Create, Read, Update, Delete) operations:
GET /users
: Retrieve a list of all users.POST /users
: Create a new user.GET /users/{user_id}
: Get a single user by their ID.DELETE /users/{user_id}
: Delete a user by their ID.
For simplicity, we will use a simple Python dictionary as our in-memory "database". In a real-world application, this would be a connection to a database like PostgreSQL or MongoDB.
Implementing the Flask Endpoints
Let's update our app.py
file to include the logic for these endpoints. Replace the content of app.py
with the following:Python
# app.py
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# --- In-Memory Database ---
# A simple dictionary to store our users.
# The key is the user_id (integer), and the value is the user data (dict).
USERS_DB = {
1: {"username": "alice", "email": "alice@example.com"},
2: {"username": "bob", "email": "bob@example.com"},
3: {"username": "charlie", "email": "charlie@example.com"},
}
# A counter to simulate auto-incrementing IDs for new users
LAST_INSERT_ID = 3
# --- API Endpoints ---
@app.route("/users", methods=["GET"])
def get_users():
""" Returns a list of all users. """
# We need to convert the dictionary to a list of user objects, including their IDs.
users_list = []
for user_id, user_data in USERS_DB.items():
user = {'id': user_id}
user.update(user_data)
users_list.append(user)
return jsonify(users_list)
@app.route("/users", methods=["POST"])
def create_user():
""" Creates a new user. """
global LAST_INSERT_ID
if not request.json or 'username' not in request.json or 'email' not in request.json:
abort(400, description="Missing username or email in request body.")
LAST_INSERT_ID += 1
new_user_id = LAST_INSERT_ID
new_user = {
"username": request.json["username"],
"email": request.json["email"],
}
USERS_DB[new_user_id] = new_user
# The response should include the ID of the newly created user
response_user = {'id': new_user_id}
response_user.update(new_user)
return jsonify(response_user), 201
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
""" Returns a single user by their ID. """
if user_id not in USERS_DB:
abort(404, description=f"User with ID {user_id} not found.")
user_data = USERS_DB[user_id]
user = {'id': user_id}
user.update(user_data)
return jsonify(user)
@app.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
""" Deletes a user by their ID. """
if user_id not in USERS_DB:
abort(404, description=f"User with ID {user_id} not found.")
del USERS_DB[user_id]
# A 204 No Content response is standard for successful deletions
return '', 204
if __name__ == "__main__":
app.run(debug=True, port=5000)
Now, if you run python app.py
again, you will have a fully functional (though simple) API. You can test it with curl
or a similar tool:
- Get all users:
curl http://127.0.0.1:5000/users
- Get a specific user:
curl http://127.0.0.1:5000/users/1
- Create a user:
curl -X POST -H "Content-Type: application/json" -d '{"username": "david", "email": "david@example.com"}' http://127.0.0.1:5000/users
- Delete a user:
curl -X DELETE http://127.0.0.1:5000/users/3
With our API implemented, we have a concrete target for our documentation efforts. The next step is to use pyswagger
to describe each of these endpoints in detail.
Part 4: Auto-Generating the OpenAPI Spec with pyswagger
This is the core of our tutorial. We will now create a separate Python script that imports pyswagger
, defines our API structure using its object model, and then serializes that structure into a complete openapi.json
file. This approach decouples the specification generation from the application logic, which can be a very clean and maintainable pattern.
Creating the Specification Generator
In your project directory, create a new file named generate_spec.py
. This script will be responsible for building and saving our OpenAPI specification.
Building the Spec, Step-by-Step
Let's build the generate_spec.py
script piece by piece.
1. Imports and Initializing the App
First, we need to import the App
object from pyswagger
and PyYAML
to help with dumping the final file. We'll create our root App
object and populate the basic info
and servers
sections, just as we discussed in Part 2.Python
# generate_spec.py
import json
from pyswagger import App
from pyswagger.contrib.client.requests import Client
# --- 1. Initialize the root App object ---
app = App(version='3.0.0')
# --- 2. Populating the Info & Servers sections ---
app.info.title = "User API"
app.info.version = "1.0.0"
app.info.description = "A simple API to manage users, for the pyswagger tutorial."
server = app.prepare_obj('Server', {
'url': 'http://127.0.0.1:5000',
'description': 'Local development server'
})
app.servers.append(server)
2. Defining Reusable Components (Schemas)
Good API design avoids repetition. We will define our User
data model once and reuse it. We'll also define a generic Error
schema for our error responses (like 404 Not Found).
Add the following code to generate_spec.py
:Python
# --- 3. Defining Reusable Components (Schemas) ---
# Schema for the Error response
error_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'code': {'type': 'integer', 'format': 'int32'},
'message': {'type': 'string'}
}
})
app.components.schemas['Error'] = error_schema
# Schema for a single User. Note the properties match our USERS_DB structure.
user_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'id': {
'type': 'integer',
'description': 'Unique identifier for the user.',
'readOnly': True # The client cannot set this value
},
'username': {
'type': 'string',
'description': 'The user\'s chosen username.'
},
'email': {
'type': 'string',
'description': 'The user\'s email address.',
'format': 'email'
}
},
'required': ['id', 'username', 'email']
})
app.components.schemas['User'] = user_schema
# Schema for creating a user (doesn't include the 'id' field)
new_user_schema = app.prepare_obj('Schema', {
'type': 'object',
'properties': {
'username': {
'type': 'string',
'description': 'The user\'s chosen username.'
},
'email': {
'type': 'string',
'description': 'The user\'s email address.',
'format': 'email'
}
},
'required': ['username', 'email']
})
app.components.schemas['NewUser'] = new_user_schema
3. Documenting the /users
Endpoints
Now we will define the /users
path and its two operations: GET
and POST
.
- For
GET
, the response is an array ofUser
objects. - For
POST
, the request body will expect aNewUser
object, and the successful response will be a singleUser
object (including the new ID).
<!-- end list -->Python
# --- 4. Documenting the Paths ---
# -- Path: /users --
path_users = app.define_path('/users')
# Operation: GET /users
op_get_users = path_users.define_op('get')
op_get_users.summary = "List all users"
op_get_users.description = "Returns a JSON array of all user objects."
op_get_users.tags.append('Users')
op_get_users.responses.A('200').description = "A list of users."
op_get_users.responses.A('200').content.A('application/json').schema.A(
'array', items={'$ref': '#/components/schemas/User'}
)
# Operation: POST /users
op_post_users = path_users.define_op('post')
op_post_users.summary = "Create a new user"
op_post_users.description = "Adds a new user to the database."
op_post_users.tags.append('Users')
op_post_users.requestBody.description = "User object that needs to be added."
op_post_users.requestBody.required = True
op_post_users.requestBody.content.A('application/json').schema.set_ref('#/components/schemas/NewUser')
op_post_users.responses.A('201').description = "User created successfully."
op_post_users.responses.A('201').content.A('application/json').schema.set_ref('#/components/schemas/User')
op_post_users.responses.A('400').description = "Invalid input provided."
op_post_users.responses.A('400').content.A('application/json').schema.set_ref('#/components/schemas/Error')
Notice the use of set_ref
and A
(which stands for "access") for a more concise syntax to build the nested object structure.
4. Documenting the /users/{user_id}
Endpoints
Next, we'll document the path for interacting with a single user. This path includes a path parameter, {user_id}
.
- For
GET
, we'll need to define this path parameter. The response is a singleUser
object or a404
error. - For
DELETE
, we also need the path parameter. The successful response is a204 No Content
.
<!-- end list -->Python
# -- Path: /users/{user_id} --
path_user_id = app.define_path('/users/{user_id}')
# We can define the parameter once and reuse it for all operations on this path.
user_id_param = app.prepare_obj('Parameter', {
'name': 'user_id',
'in': 'path',
'description': 'ID of the user',
'required': True,
'schema': {'type': 'integer'}
})
path_user_id.parameters.append(user_id_param)
# Operation: GET /users/{user_id}
op_get_user_id = path_user_id.define_op('get')
op_get_user_id.summary = "Find user by ID"
op_get_user_id.description = "Returns a single user."
op_get_user_id.tags.append('Users')
op_get_user_id.responses.A('200').description = "Successful operation."
op_get_user_id.responses.A('200').content.A('application/json').schema.set_ref('#/components/schemas/User')
op_get_user_id.responses.A('404').description = "User not found."
op_get_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')
# Operation: DELETE /users/{user_id}
op_delete_user_id = path_user_id.define_op('delete')
op_delete_user_id.summary = "Deletes a user"
op_delete_user_id.description = "Deletes a single user from the database."
op_delete_user_id.tags.append('Users')
op_delete_user_id.responses.A('204').description = "User deleted successfully."
op_delete_user_id.responses.A('404').description = "User not found."
op_delete_user_id.responses.A('404').content.A('application/json').schema.set_ref('#/components/schemas/Error')
5. Validating and Saving the Specification
Finally, the most satisfying step. We will ask pyswagger
to validate our constructed object graph against the OpenAPI 3.0 schema. If it's valid, we'll dump it to a JSON file.
Add this final block of code to generate_spec.py
:Python
# --- 5. Validate and Save the Specification ---
if __name__ == '__main__':
try:
# Validate the generated specification
app.validate()
print("Specification is valid.")
# Save the specification to a JSON file
with open('openapi.json', 'w') as f:
f.write(app.dump_json(indent=2))
print("Successfully generated openapi.json")
except Exception as e:
print(f"Validation Error: {e}")
Your generate_spec.py
file is now complete. Run it from your terminal:Bash
python generate_spec.py
If everything is correct, you will see the output:
Specification is valid.
Successfully generated openapi.json
You will now have a new file, openapi.json
, in your project directory. Open it up and explore its contents. You'll see a perfectly structured OpenAPI 3.0 document that describes your Flask API in minute detail.
Part 5: Serving the Documentation
Having an openapi.json
file is great for machines and for generating client SDKs, but for human developers, an interactive UI is far more useful. In this final part, we will integrate our generated spec into our Flask app and serve it using the popular Swagger UI.
Integrating the Spec into Flask
First, we need to create an endpoint in our Flask app that serves the openapi.json
file. This allows documentation tools to fetch the spec directly from our running API.
Modify app.py
to add a new route:Python
# app.py
# ... (keep all the existing Flask code) ...
import os
# ... (all the routes like /users, /users/{user_id}, etc.) ...
# --- Serving the OpenAPI Specification and UI ---
@app.route('/api/docs/openapi.json')
def serve_openapi_spec():
""" Serves the generated openapi.json file. """
# This assumes openapi.json is in the same directory as app.py
# In a larger app, you might want a more robust way to find the file
return app.send_static_file('openapi.json')
For this to work, Flask needs to know where to find static files. By default, it looks in a static
directory. Let's create one and move our openapi.json
file there.Bash
mkdir static
mv openapi.json static/
Now, run python app.py
, and navigate to http://127.0.0.1:5000/api/docs/openapi.json
. You should see the raw JSON of your specification.
Serving Interactive UI with Swagger UI 🎨
Swagger UI is a dependency-free collection of HTML,2 JavaScript, and CSS assets that dynamically generates beautiful documentation from an OpenAPI-compliant API. We can easily serve it from our Flask application.
Create a Templates Directory: Flask uses a directory named templates
to look for HTML templates.Bash
mkdir templates
Create the Swagger UI Template: Inside the templates
directory, create a new file called swagger_ui.html
. Paste the following HTML code into it. This code loads the Swagger UI assets from a public CDN and configures it to load our spec file.HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>User API - Swagger UI</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.min.css" >
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger3 UI call region
const ui = SwaggerUIBundle({
url: "/api/docs/openapi.json", // The URL for our spec
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset4
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
```
Add the Flask Route: Finally, add a route to app.py
to render this HTML template. You'll need to import render_template
from Flask.Python
# At the top of app.py
from flask import Flask, jsonify, request, abort, render_template
# ... (all other routes) ...
@app.route('/api/docs/')
def serve_swagger_ui():
""" Serves the interactive Swagger UI documentation. """
return render_template('swagger_ui.html')
Putting It All Together
Your final project structure should look like this:
pyswagger-tutorial/
├── app.py
├── generate_spec.py
├── requirements.txt
├── static/
│ └── openapi.json
├── templates/
│ └── swagger_ui.html
└── venv/
Now for the final result. Make sure your Flask app is running (python app.py
). Open your web browser and navigate to:
http://127.0.0.1:5000/api/docs/
You should be greeted with a beautiful, interactive documentation page for your API. You can expand each endpoint, see the schemas you defined, and even use the "Try it out" button to send live requests to your running Flask application directly from the browser.
Conclusion
In this tutorial, we have journeyed from an empty directory to a fully documented API. We successfully used pyswagger
to programmatically construct a detailed OpenAPI 3.0 specification by mapping its object model to our API's structure. We saw how to define metadata, servers, reusable schemas, paths, and operations with their parameters and responses.
By separating the spec generation (generate_spec.py
) from the application logic (app.py
), we created a clean workflow: implement your API, then run the generator to produce the documentation. This process, while more manual than decorator-based approaches, offers unparalleled control and flexibility, making it ideal for complex projects, legacy codebases, or when documentation standards are strict.
Finally, by serving the generated spec and a Swagger UI instance, we provided a polished, professional, and highly usable documentation portal for our API's consumers. You now have a powerful new tool in your Python development arsenal for creating world-class API documentation.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demands, and replaces Postman at a much more affordable price!