azure-functions-openapi-pydantic 1.0.0b1
Lightweight OpenAPI decorator for Azure Functions with Pydantic support
License: MIT
Requires Python: >=3.8
Keywords: azure, functions, openapi, pydantic, swagger

# 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

```bash
pip install azure-functions-openapi-pydantic
```

## Quick Start

```python
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:**
```json
{
  "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:

```python
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:**
```json
{
  "200": {
    "content": {
      "application/json": {
        "schema": {
          "properties": {
            "success": {"type": "boolean"},
            "data": {"$ref": "#/components/schemas/User"}
          }
        }
      }
    }
  }
}
```

## Generate OpenAPI Spec & Swagger UI

```python
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

```python
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:

```python
@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:

```python
@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:

```python
@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:

```python
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:

```python
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:

```python
# 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/`](./examples) directory contains complete Azure Functions applications:

### [Basic User API](./examples/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](./examples/bol-extraction-service/)
Complex real-world API demonstrating:
- Multiple blueprints
- Complex nested models
- Blueprint-level organization
- Production-ready structure

```bash
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](./examples/README.md) for more details.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Contributing

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