Skip to main content

Lightweight OpenAPI decorator for Azure Functions with Pydantic support

Project description

Azure Functions OpenAPI with Pydantic

A lightweight, zero-opinion OpenAPI decorator for Azure Functions with full Pydantic support. Generate accurate API documentation with minimal code.

Features

  • 🎯 Simple decorator syntax - Just add @openapi() to your functions
  • 📝 Pydantic integration - Full validation and schema generation
  • 🔓 Zero opinions - Return models directly or use your own envelope pattern
  • 🏗️ Nested models - Automatically expands nested Pydantic models
  • Blueprint support - Works seamlessly with Azure Functions blueprints
  • 🏷️ Smart tag inference - Auto-organizes endpoints by analyzing routes
  • 📊 Docstring introspection - Automatically extracts summaries from docstrings

Installation

pip install azure-functions-openapi-pydantic

Quick Start

import azure.functions as func
from pydantic import BaseModel, EmailStr
from azfunc_openapi_pydantic import openapi, OpenAPIBlueprint

# Define your models
class User(BaseModel):
    id: str
    name: str
    email: EmailStr

# Create blueprint
api_bp = OpenAPIBlueprint()

# Direct response (no envelope)
@api_bp.route(route="users/{id}", methods=["GET"])
@openapi(responses={200: User, 404: dict})
def get_user(req: func.HttpRequest) -> func.HttpResponse:
    """Get a user by ID"""
    user = User(id="123", name="Alice", email="alice@example.com")
    
    return func.HttpResponse(
        json.dumps(user.model_dump()),
        mimetype="application/json"
    )

# Register blueprint
app = func.FunctionApp()
app.register_functions(api_bp)

OpenAPI spec shows:

{
  "paths": {
    "users/{id}": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/User"}
              }
            }
          }
        }
      }
    }
  }
}

Optional: Define Your Own Envelope Pattern

The library doesn't force any response structure. If you want a consistent envelope, define it yourself:

from typing import Generic, TypeVar, Optional
from pydantic import BaseModel

T = TypeVar('T')

class ApiResponse(BaseModel, Generic[T]):
    """Your custom envelope"""
    success: bool
    data: Optional[T] = None
    error: Optional[dict] = None
    
    @classmethod
    def ok(cls, data, status_code=200):
        return func.HttpResponse(
            json.dumps({"success": True, "data": data.model_dump()}),
            mimetype="application/json",
            status_code=status_code
        )

# Use it
@openapi(responses={200: ApiResponse[User]})
def get_user(req: func.HttpRequest):
    """Get user with envelope"""
    user = User(id="123", name="Alice", email="alice@example.com")
    return ApiResponse.ok(user)  # → {success: true, data: {...}}

OpenAPI spec shows your envelope:

{
  "200": {
    "content": {
      "application/json": {
        "schema": {
          "properties": {
            "success": {"type": "boolean"},
            "data": {"$ref": "#/components/schemas/User"}
          }
        }
      }
    }
  }
}

Generate OpenAPI Spec & Swagger UI

from azfunc_openapi_pydantic import generate_openapi_spec, generate_swagger_ui

@app.route(route="openapi.json", methods=["GET"])
def openapi_spec(req: func.HttpRequest) -> func.HttpResponse:
    """OpenAPI 3.0 spec"""
    spec = generate_openapi_spec(
        title="My API",
        version="1.0.0",
        description="API documentation"
    )
    return func.HttpResponse(
        json.dumps(spec, indent=2),
        mimetype="application/json"
    )

@app.route(route="docs", methods=["GET"])
def swagger_ui(req: func.HttpRequest) -> func.HttpResponse:
    """Interactive Swagger UI"""
    return generate_swagger_ui(
        title="My API Documentation",
        openapi_url="/api/openapi.json"
    )

Visit: http://localhost:7071/api/docs

Decorator Options

from azfunc_openapi_pydantic import openapi, ResponsesMap

@openapi(
    request=CreateUserRequest,       # Request body model
    responses={                       # Status code → Response model
        200: User,
        201: User,
        400: ErrorDetail,
        404: NotFoundError
    },
    # Or use ResponsesMap for validation:
    responses=ResponsesMap({
        200: User,
        404: NotFoundError
    }),
    params={                          # Query/path parameters
        "limit": int,
        "offset": int,
        "active": bool
    },
    tags=["Users"]                    # Optional (auto-inferred if omitted)
)
def my_endpoint(req: func.HttpRequest):
    """
    This docstring becomes the OpenAPI summary.
    
    Additional lines become the description.
    """
    pass

Request Validation

The decorator automatically validates request bodies:

@openapi(
    request=CreateUserRequest,
    responses={201: User, 400: dict}
)
def create_user(req: func.HttpRequest) -> func.HttpResponse:
    """Create user with automatic validation"""
    # Validated body is attached to the request
    body = req.validated_body
    
    user = User(
        id=generate_id(),
        name=body.name,
        email=body.email,
        age=body.age
    )
    
    return func.HttpResponse(
        json.dumps(user.model_dump()),
        mimetype="application/json",
        status_code=201
    )

Invalid requests return automatic 400 responses with validation errors.

Parameter Injection

Instead of accessing req, inject validated parameters directly:

@openapi(
    request=CreateUserRequest,
    responses={201: User}
)
def create_user(body: CreateUserRequest) -> func.HttpResponse:
    """Body is injected and validated"""
    user = User(id=generate_id(), **body.model_dump())
    return func.HttpResponse(json.dumps(user.model_dump()))

Query parameters work too:

@openapi(
    responses={200: list[User]},
    params={"limit": int, "offset": int}
)
def list_users(limit: int = 10, offset: int = 0) -> func.HttpResponse:
    """Parameters are injected and type-converted"""
    users = get_users(limit=limit, offset=offset)
    return func.HttpResponse(json.dumps([u.model_dump() for u in users]))

Nested Models

Nested Pydantic models are automatically expanded:

class Address(BaseModel):
    street: str
    city: str
    country: str = "US"

class UserProfile(BaseModel):
    user: User
    address: Address
    preferences: dict

@openapi(responses={200: UserProfile})
def get_profile(req: func.HttpRequest):
    """Returns fully expanded nested schema"""
    pass

Blueprints

Organize your API with blueprints:

from azfunc_openapi_pydantic import OpenAPIBlueprint

# Create blueprint with default tags (optional)
users_bp = OpenAPIBlueprint(tags=["Users"])

@users_bp.route(route="users", methods=["GET"])
@openapi(responses={200: list[User]})
def list_users(req: func.HttpRequest):
    """Inherits 'Users' tag from blueprint"""
    pass

@users_bp.route(route="users/{id}", methods=["GET"])
@openapi(
    responses={200: User},
    tags=["Admin"]  # Override blueprint tag
)
def get_user(req: func.HttpRequest):
    """Uses 'Admin' tag instead"""
    pass

# Register
app.register_functions(users_bp)

Smart Tag Inference

If you don't specify tags, they're automatically inferred from your routes:

# Routes: api/v1/users, api/v1/users/{id}, api/v1/products
# → Tags: "Users", "Products" (inferred from diverging path segments)

# Routes: users, users/{id}, users/{id}/profile
# → Tag: "Users" (sub-resources grouped under parent)

Explicit tags always take precedence over inference.

Examples

The examples/ directory contains complete Azure Functions applications:

Basic User API

Simple user management API demonstrating core features:

  • Direct responses vs. envelope pattern
  • Request validation and parameter injection
  • OpenAPI spec and Swagger UI generation

BOL Extraction Service

Complex real-world API demonstrating:

  • Multiple blueprints
  • Complex nested models
  • Blueprint-level organization
  • Production-ready structure
cd examples/basic-user-api  # or bol-extraction-service
pip install -r requirements.txt
func start
# Visit http://localhost:7071/api/docs

See examples/README.md for more details.

License

MIT License - see LICENSE for details.

Contributing

Contributions welcome! This library aims to stay lightweight and unopinionated. Please open an issue before starting work on major features.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

azure_functions_openapi_pydantic-1.0.0b1.tar.gz (31.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file azure_functions_openapi_pydantic-1.0.0b1.tar.gz.

File metadata

File hashes

Hashes for azure_functions_openapi_pydantic-1.0.0b1.tar.gz
Algorithm Hash digest
SHA256 c290abb6c102a80c1c11a69479deaf4301656756381de899ea628628ced46193
MD5 1739b44ebfc195f7edcc699dcc79b570
BLAKE2b-256 503d479f20fa95225412fa036437d0a5a760231ad87274d5dda197fe7b6ea207

See more details on using hashes here.

File details

Details for the file azure_functions_openapi_pydantic-1.0.0b1-py3-none-any.whl.

File metadata

File hashes

Hashes for azure_functions_openapi_pydantic-1.0.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 f396031e56e6eccc37619837b6621ab17534d71beb9ecdddc635d6796c43380f
MD5 ef0f82bdc185d4240fdd0d969c6e9fbe
BLAKE2b-256 1490d8e1821a8b548c908440969c5815a67111eb7dbbf2f45e942503de922b18

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page