Skip to content

Latest commit

 

History

History
561 lines (483 loc) · 13.7 KB

File metadata and controls

561 lines (483 loc) · 13.7 KB

7. OpenAPI Generation

!!! success "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

!!! success "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