7. OpenAPI Generation
Unique Feature
Built-in OpenAPI generation is rare in JSON-RPC libraries! Most require manual schema writing or have no documentation features at all.
What You'll Learn
- Generate OpenAPI 3.0 schema from your methods
- Integrate with RapiDoc/Swagger UI
- Auto-document complex types and nested structures
Basic OpenAPI Generation
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
from jsonrpc.openapi import OpenAPIGenerator
# Define methods
@dataclass
class CalculateParams:
x: float
y: float
operation: str
class Calculate(Method):
def execute(self, params: CalculateParams) -> float:
"""Perform arithmetic operation on two numbers.
Supported operations: add, subtract, multiply, divide
"""
ops = {
'add': params.x + params.y,
'subtract': params.x - params.y,
'multiply': params.x * params.y,
'divide': params.x / params.y if params.y != 0 else 0.0
}
return ops.get(params.operation, 0.0)
@dataclass
class GreetParams:
name: str
greeting: str = "Hello"
class Greet(Method):
def execute(self, params: GreetParams) -> str:
"""Greet someone with a customizable greeting message."""
return f"{params.greeting}, {params.name}!"
# Setup
rpc = JSONRPC(version='2.0')
rpc.register('calculate', Calculate())
rpc.register('greet', Greet())
# Generate OpenAPI schema
generator = OpenAPIGenerator(
rpc,
title="Calculator API",
version="1.0.0",
description="Simple calculator with JSON-RPC 2.0"
)
spec = generator.generate()
# spec is a dict containing full OpenAPI 3.0 schema
import json
print(json.dumps(spec, indent=2))
Generated OpenAPI Schema (abbreviated):
Each method gets its own path using fragment syntax (#method.name), with separate
request and response schemas in components/schemas:
{
"openapi": "3.0.3",
"info": {
"title": "Calculator API",
"version": "1.0.0",
"description": "Simple calculator with JSON-RPC 2.0"
},
"paths": {
"/jsonrpc#math.calculate": {
"post": {
"operationId": "math_calculate",
"summary": "Perform a math operation.",
"tags": ["math"],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/math.calculate_request"}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/math.calculate_response"}
}
}
},
"default": {
"description": "JSON-RPC Error",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/JSONRPCError"}
}
}
}
}
}
}
},
"components": {
"schemas": {
"math.calculate_request": {
"type": "object",
"properties": {
"jsonrpc": {"const": "2.0"},
"method": {"const": "math.calculate"},
"id": {"type": "integer"},
"params": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"operation": {"type": "string"}
},
"required": ["x", "y", "operation"]
}
},
"required": ["jsonrpc", "method", "id", "params"]
}
}
}
}
Complex Types and Nested Structures
from dataclasses import dataclass
from typing import Literal
from jsonrpc import JSONRPC, Method
from jsonrpc.openapi import OpenAPIGenerator
@dataclass
class Address:
street: str
city: str
country: str
@dataclass
class Contact:
email: str
phone: str | None = None
@dataclass
class CreateUserParams:
username: str
email: str
age: int
address: Address
contacts: list[Contact]
role: Literal["user", "admin", "moderator"] = "user"
tags: list[str] | None = None
@dataclass
class CreateUserResult:
user_id: int
username: str
role: str
class CreateUser(Method):
def execute(self, params: CreateUserParams) -> CreateUserResult:
"""Create a new user with full profile information.
Returns the created user ID and profile summary.
"""
return CreateUserResult(
user_id=123,
username=params.username,
role=params.role,
)
rpc = JSONRPC(version='2.0')
rpc.register('create_user', CreateUser())
generator = OpenAPIGenerator(rpc, title="User Management API", version="1.0.0")
spec = generator.generate()
# OpenAPI schema includes:
# - Nested Address schema
# - Nested Contact schema
# - Literal enum for role
# - Optional types (phone, tags)
# - Array types (contacts, tags)
Generated Schema (excerpt):
{
"components": {
"schemas": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"},
"country": {"type": "string"}
},
"required": ["street", "city", "country"]
},
"Contact": {
"type": "object",
"properties": {
"email": {"type": "string"},
"phone": {"type": "string", "nullable": true}
},
"required": ["email"]
},
"CreateUserParams": {
"type": "object",
"properties": {
"username": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "integer"},
"address": {"$ref": "#/components/schemas/Address"},
"contacts": {
"type": "array",
"items": {"$ref": "#/components/schemas/Contact"}
},
"role": {
"type": "string",
"enum": ["user", "admin", "moderator"],
"default": "user"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"nullable": true
}
},
"required": ["username", "email", "age", "address", "contacts"]
}
}
}
}
RapiDoc Integration (Flask)
from flask import Flask
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
from jsonrpc.openapi import OpenAPIGenerator
import json
app = Flask(__name__)
# Define API
@dataclass
class SearchParams:
query: str
limit: int = 10
class Search(Method):
def execute(self, params: SearchParams) -> list[dict]:
"""Search for items by query string."""
return [{"id": 1, "title": "Result 1"}]
rpc = JSONRPC(version='2.0')
rpc.register('search', Search())
# Generate OpenAPI spec
generator = OpenAPIGenerator(
rpc,
base_url="/rpc",
title="Search API",
version="1.0.0",
)
openapi_spec = generator.generate()
# RPC endpoint
@app.route('/rpc', methods=['POST'])
def handle_rpc():
from flask import request
response = rpc.handle(request.data)
return response, 200, {'Content-Type': 'application/json'}
# OpenAPI spec endpoint
@app.route('/openapi.json')
def openapi():
return openapi_spec
# Interactive documentation with RapiDoc
@app.route('/docs')
def docs():
return f'''
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/openapi.json"
render-style="read"
theme="dark"
show-header="false"
allow-try="true"
> </rapi-doc>
</body>
</html>
'''
if __name__ == '__main__':
app.run(port=5000)
# Visit http://localhost:5000/docs for interactive API docs!
FastAPI Integration with RapiDoc
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
from jsonrpc.openapi import OpenAPIGenerator
app = FastAPI()
# Define methods
@dataclass
class Item:
name: str
price: float
quantity: int
@dataclass
class CreateOrderParams:
items: list[Item]
customer_id: int
@dataclass
class CreateOrderResult:
order_id: str
customer_id: int
total: float
items_count: int
class CreateOrder(Method):
def execute(self, params: CreateOrderParams) -> CreateOrderResult:
"""Create a new order with items.
Returns order ID and total price.
"""
total = sum(item.price * item.quantity for item in params.items)
return CreateOrderResult(
order_id='ORD-123',
customer_id=params.customer_id,
total=total,
items_count=len(params.items),
)
rpc = JSONRPC(version='2.0')
rpc.register('create_order', CreateOrder())
# Generate OpenAPI
generator = OpenAPIGenerator(
rpc,
base_url="/rpc",
title="Order Management API",
version="2.0.0",
)
openapi_spec = generator.generate()
@app.post('/rpc')
async def handle_rpc(request: Request):
body = await request.body()
response = await rpc.handle_async(body)
return response
@app.get('/openapi.json')
async def get_openapi():
return openapi_spec
@app.get('/docs', response_class=HTMLResponse)
async def docs():
return '''
<!DOCTYPE html>
<html>
<head>
<title>Order API Documentation</title>
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/openapi.json"
render-style="focused"
theme="light"
primary-color="#7C4DFF"
allow-try="true"
allow-server-selection="false"
> </rapi-doc>
</body>
</html>
'''
Visit http://localhost:8000/docs for interactive API documentation where you can:
- Browse all available methods
- See parameter schemas
- Try requests directly in browser
- View response examples
Security Schemes
from jsonrpc.openapi import OpenAPIGenerator
# Bearer token authentication
generator = OpenAPIGenerator(rpc, title="Secure API", version="1.0.0")
generator.add_security_scheme(
"BearerAuth",
scheme_type="http",
options={"scheme": "bearer", "bearerFormat": "JWT"},
)
generator.add_security_requirement("BearerAuth")
# API Key
generator = OpenAPIGenerator(rpc, title="API Key API", version="1.0.0")
generator.add_security_scheme(
"ApiKeyAuth",
scheme_type="apiKey",
options={"name": "X-API-Key", "in": "header"},
)
generator.add_security_requirement("ApiKeyAuth")
# OAuth2
generator = OpenAPIGenerator(rpc, title="OAuth API", version="1.0.0")
generator.add_security_scheme(
"OAuth2",
scheme_type="oauth2",
options={
"flows": {
"authorizationCode": {
"authorizationUrl": "https://example.com/oauth/authorize",
"tokenUrl": "https://example.com/oauth/token",
"scopes": {
"read": "Read access",
"write": "Write access",
},
}
},
},
)
generator.add_security_requirement("OAuth2", scopes=["read", "write"])
Custom Headers
generator = OpenAPIGenerator(rpc, title="API with Headers", version="1.0.0")
generator.add_header(
name="X-User-ID",
description="User ID from session",
required=True,
schema={"type": "integer"},
)
generator.add_header(
name="X-Request-ID",
description="Unique request identifier",
required=False,
schema={"type": "string", "format": "uuid"},
)
Why This is Powerful
Traditional approach (manual schema):
# Define method
def calculate(x, y, op):
return x + y
# Separately maintain OpenAPI schema
openapi = {
"paths": {
"/": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
# Manually write schema...
# Must keep in sync with code!
}
}
}
}
}
}
}
}
python-jsonrpc-lib approach:
# Define method with types
@dataclass
class CalculateParams:
x: float
y: float
operation: str
class Calculate(Method):
def execute(self, params: CalculateParams) -> float:
"""Perform arithmetic operation."""
return params.x + params.y
# Schema auto-generated from types!
spec = generator.generate()
Benefits:
| Manual Schema | python-jsonrpc-lib |
|---|---|
| Write schema separately | Auto-generated |
| Keep docs in sync manually | Always in sync |
| No validation | Automatic validation |
| Prone to errors | Type-safe |
| Verbose | Concise |
Key Points
- Automatic: Schema generated from dataclass type hints
- Nested types: Full support for complex structures
- Docstrings: Method descriptions from docstrings
- Interactive docs: RapiDoc/Swagger UI integration
- Always in sync: Schema updates when code changes
- Zero config: Just define types, get documentation
Rare Feature
Most JSON-RPC libraries don't have OpenAPI generation. This is a unique advantage of python-jsonrpc-lib!
What's Next?
→ Integrations - Use with Flask and FastAPI
→ Advanced Topics - Async, batch, protocols