# azure-functions-validation — Full LLM Reference

> Pydantic-based request/response validation for Azure Functions Python v2.

## Package Info

- PyPI: `pip install azure-functions-validation`
- Version: see package metadata
- Python: >=3.11, <3.15
- License: MIT
- Docs: https://yeongseon.dev/azure-functions-python/validation/
- Repository: https://github.com/yeongseon/azure-functions-validation-python
- Azure Functions programming model: v2

## Installation

```bash
pip install azure-functions-validation
```

For local development:
```bash
git clone https://github.com/yeongseon/azure-functions-validation-python.git
cd azure-functions-validation-python
pip install -e .[dev]
```

## Public API

Custom adapter authors can import `ValidationAdapter`, `AdapterValidationError`,
`MalformedRequestError`, `SerializationError`, and `AdapterConformanceTests`.
The suite is pytest-agnostic at import time; subclass it in a test module and
override `make_adapter()`. Non-Pydantic backends also override its model hooks.

### Main Decorator

```python
from azure_functions_validation import validate_http
def validate_http(
    *,
    body: Any = None,
    query: Any = None,
    path: Any = None,
    headers: Any = None,
    request_model: Any = None,
    response_model: Any = None,
    adapter: ValidationAdapter | None = None,
    error_formatter: ErrorFormatter | None = None,
    expose_internal_errors: bool = False,
    status_code: int = 200,
    legacy_loc: bool = False,
    response_by_alias: bool = True,
    response_exclude_none: bool = False,
    response_exclude_unset: bool = False,
    max_body_bytes: int | None = None,
    require_json_content_type: bool = False,
    aggregate_validation_errors: bool = False,
) -> Callable[..., Any]:
    """Decorator for validating HTTP request inputs and response outputs.
    
    Args:
        body: Pydantic model for request body validation.
        query: Pydantic model for query parameter validation.
        path: Pydantic model for path parameter validation.
        headers: Pydantic model for header validation.
        request_model: Deprecated shorthand alias for body. Use body instead.
        response_model: Pydantic model or TypeAdapter-compatible type for response validation.
        adapter: Custom validation adapter (defaults to PydanticAdapter).
        error_formatter: Per-handler custom error formatter.
        expose_internal_errors: Unsafe opt-in to pass original 5xx exceptions.
        status_code: HTTP status code for successful responses (default 200).
        legacy_loc: Omit the leading input source from error locations when true.
        response_by_alias: Use aliases in validated response JSON and schema metadata.
        response_exclude_none: Omit None-valued fields from validated response JSON.
        response_exclude_unset: Omit fields not explicitly set from validated response JSON.
        max_body_bytes: Optional positive application-level body byte limit (HTTP 413).
        require_json_content_type: Require JSON media types for non-empty bodies (HTTP 415).
        aggregate_validation_errors: Collect all configured request-source validation errors in one 422.
    
    Returns:
        A decorator that wraps the handler with validation logic.
    """
```

### Error Classes

```python
from azure_functions_validation import (
    InternalServerError,
    MalformedRequestError,
    PayloadTooLargeError,
    ResponseValidationError,
    SerializationError,
    UnsupportedMediaTypeError,
)

class ResponseValidationError(Exception):
    """Raised when response validation fails.
    
    Attributes:
        message: Error message describing the validation failure.
    """
    def __init__(self, message: str = "Response validation error"):
        pass

class SerializationError(TypeError):
    """Raised when an unsupported type is encountered during serialization.
    
    Attributes:
        type_name: Name of the unsupported type.
    """
    def __init__(self, type_name: str) -> None:
        pass

class MalformedRequestError(Exception):
    """Raised by adapters for syntactically malformed client input (HTTP 400)."""

class PayloadTooLargeError(Exception):
    """Raised when max_body_bytes is exceeded (HTTP 413)."""

class UnsupportedMediaTypeError(Exception):
    """Raised when require_json_content_type rejects a media type (HTTP 415)."""
```

### Error Formatting

```python
from azure_functions_validation import ErrorFormatter
from typing import Any

ErrorFormatter = Callable[[Exception, int], dict[str, Any]]
```

Type alias for custom error formatters. Called with:
- `exception`: Original for 4xx; sanitized `InternalServerError` for 5xx by default
- `status_code`: HTTP status code (400, 422, 500, etc.)

Must return a dict representing the error response body (will be JSON-serialized).

## Common Patterns

### Strict API Boundary Models

Plain Pydantic models use lax coercion and `extra="ignore"`. For closed JSON
body contracts, derive request models from a base with
`ConfigDict(strict=True, extra="forbid")`. Do not automatically reuse it for
query, path, or header models: Azure Functions supplies those values as strings,
so model-wide strictness rejects inputs such as `?limit=10` for `limit: int`.
Use `ConfigDict(extra="forbid")` without `strict=True` for those sources when
normal string parsing is required. Full runnable examples and exact 422 bodies:
https://yeongseon.dev/azure-functions-python/validation/strict-models/

### Basic Request Body Validation

```python
import azure.functions as func
from pydantic import BaseModel
from azure_functions_validation import validate_http

class UserCreateRequest(BaseModel):
    name: str
    email: str

app = func.FunctionApp()

@app.route(route="users", methods=["POST"])
@validate_http(body=UserCreateRequest)
def create_user(req: func.HttpRequest, body: UserCreateRequest) -> func.HttpResponse:
    # body is already validated UserCreateRequest instance
    return func.HttpResponse(f"Created {body.name}", status_code=201)
```

### Query and Path Parameters

```python
from pydantic import BaseModel

class SearchParams(BaseModel):
    q: str
    limit: int = 10
    offset: int = 0

class UserPathParams(BaseModel):
    user_id: str

@app.route(route="users/<user_id>")
@validate_http(path=UserPathParams, query=SearchParams)
def get_user(req: func.HttpRequest, path: UserPathParams, query: SearchParams) -> func.HttpResponse:
    # path and query are validated instances
    return func.HttpResponse(f"User: {path.user_id}, limit: {query.limit}")
```

### Request + Response Validation

```python
class CreateUserResponse(BaseModel):
    id: str
    name: str
    created_at: str

@app.route(route="users", methods=["POST"])
@validate_http(body=UserCreateRequest, response_model=CreateUserResponse)
def create_user(req: func.HttpRequest, body: UserCreateRequest) -> CreateUserResponse:
    # Response must match CreateUserResponse schema, or ResponseValidationError is raised
    return CreateUserResponse(
        id="123",
        name=body.name,
        created_at="2024-01-01T00:00:00Z"
    )
```

### Custom Error Formatting

```python
from azure_functions_validation import ErrorFormatter

def my_error_formatter(exception: Exception, status_code: int) -> dict:
    message = str(exception) if status_code < 500 else "Internal Server Error"
    return {
        "code": "VALIDATION_ERROR",
        "message": message,
        "status": status_code,
    }

@app.route(route="users", methods=["POST"])
@validate_http(
    body=UserCreateRequest,
    error_formatter=my_error_formatter
)
def create_user_custom_errors(
    req: func.HttpRequest,
    body: UserCreateRequest
) -> func.HttpResponse:
    return func.HttpResponse(f"Created {body.name}")
```

### Header Validation

```python
from pydantic import BaseModel, ConfigDict, Field

class HeaderParams(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    authorization: str = Field(alias="authorization")
    x_user_id: str | None = Field(default=None, alias="x-user-id")

@app.route(route="protected")
@validate_http(headers=HeaderParams)
def protected_endpoint(
    req: func.HttpRequest,
    headers: HeaderParams
) -> func.HttpResponse:
    # headers.authorization is validated
    return func.HttpResponse("OK")
```

## Design Principles

1. **Decorator-first**: Configuration happens at decoration time, validation at runtime.
2. **Explicit over implicit**: All parameters are explicit in decorator arguments.
3. **Pydantic v2 native**: Leverages Pydantic's validation power and error messages.
4. **Error consistency**: Malformed syntax returns `400`, validation failures return `422`, and binding or adapter faults return sanitized `500` responses.
5. **Response enforcement**: Response models catch contract drift before deployment.
6. **Minimal overhead**: No request processing happens until handler is called.
7. **Azure Functions v2 aligned**: Follows Azure Functions Python v2 programming model conventions.

## Limitations

1. **Request type**: Only works with `azure.functions.HttpRequest`.
2. **Response types**: Supports anything JSON-serializable (BaseModel, dict, list, str, int, bool, None).
3. **Async support**: Both sync and async handlers are supported.
4. **No inheritance validation**: Inherited Pydantic fields work, but validation model itself cannot be inherited in decorator.
5. **File uploads**: Not designed for multipart/form-data or file uploads. Use raw request handling for that.

## Error Response Format

All validation errors return `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}` format
matching Pydantic ValidationError style. This is consistent across all handlers unless overridden
with a custom `ErrorFormatter`.

Example validation error:
```json
{
  "detail": [
    {
      "loc": ["email"],
      "msg": "value is not a valid email address",
      "type": "value_error.email"
    }
  ]
}
```

## Type Hints

All public functions and classes are fully type-hinted for IDE support and static analysis.

```python
from typing import Any, Callable
from pydantic import BaseModel

ErrorFormatter: type = Callable[[Exception, int], dict[str, Any]]
```

Supported parameter types in decorator:
- `body`, `query`, `path`, `headers`: Any Pydantic `BaseModel` subclass
- `response_model`: Pydantic `BaseModel` subclass, or any TypeAdapter-compatible type (e.g. `list[Model]`)
- `error_formatter`: Callable matching `ErrorFormatter` signature
- `adapter`: Custom validation adapter implementing `ValidationAdapter` protocol

Custom adapter parsing methods must raise `MalformedRequestError` for malformed
client syntax (`400`) or `AdapterValidationError` for validation failures
(`422`). Every other exception, including plain `ValueError`, is an adapter fault
that is logged with traceback and returned as a sanitized `500`.

Adapters are instantiated once at decoration and reused across concurrent
invocations, so they must be stateless or thread-safe. `serialize()` returns a
`(str | bytes, content_type: str)` pair and raises `SerializationError` for
unsupported values; the pipeline handles `None` as 204 before serialization.
Protocol signatures, exception/status classification, normalized error shape,
content types, and reuse semantics are compatibility contracts. While the
package is pre-1.0, incompatible protocol changes require a minor release.
