Skip to content

API Reference

This page documents the public API exported from azure_functions_validation.

from azure_functions_validation import (
    ErrorFormatter,
    ResponseValidationError,
    SerializationError,
    validate_http,
)

Public surface

The package exports: validate_http, ResponseValidationError, ErrorFormatter, and SerializationError. Pipeline and adapter internals are not public contracts.

validate_http

Decorator for validating HTTP request inputs and response outputs.

Parameters:

Name Type Description Default
body Any

Pydantic model for request body validation.

None
query Any

Pydantic model for query parameter validation.

None
path Any

Pydantic model for path parameter validation.

None
headers Any

Pydantic model for header validation.

None
request_model Any

Deprecated shorthand alias for body. Use body instead; passing request_model emits a DeprecationWarning.

None
response_model Any

Pydantic model for response validation.

None
adapter ValidationAdapter | None

Custom validation adapter (defaults to PydanticAdapter).

None
error_formatter ErrorFormatter | None

Per-handler custom error formatter.

None
status_code int

HTTP status code for successful responses (default 200). Use e.g. status_code=201 for creation endpoints.

200
legacy_loc bool

When True, error loc values omit the leading input-source segment (["email"] instead of ["body", "email"]). A one-cycle migration escape hatch; ignored when a custom adapter is supplied (configure that adapter directly).

False

Returns:

Type Description
Callable[..., Any]

A decorator that wraps the handler with validation logic.

Source code in src/azure_functions_validation/decorator.py
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,
    status_code: int = 200,
    legacy_loc: 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;
            passing ``request_model`` emits a ``DeprecationWarning``.
        response_model: Pydantic model for response validation.
        adapter: Custom validation adapter (defaults to ``PydanticAdapter``).
        error_formatter: Per-handler custom error formatter.
        status_code: HTTP status code for successful responses (default 200).
            Use e.g. ``status_code=201`` for creation endpoints.
        legacy_loc: When ``True``, error ``loc`` values omit the leading
            input-source segment (``["email"]`` instead of ``["body", "email"]``).
            A one-cycle migration escape hatch; ignored when a custom *adapter*
            is supplied (configure that adapter directly).

    Returns:
        A decorator that wraps the handler with validation logic.
    """
    # Handle request_model shorthand
    if request_model is not None:
        warnings.warn(
            "The 'request_model' parameter of validate_http() is deprecated in favor "
            "of 'body' and will be removed in a future release. See "
            "https://github.com/yeongseon/azure-functions-validation-python/issues/223.",
            DeprecationWarning,
            stacklevel=2,
        )
        if any([body, query, path, headers]):
            raise ValueError("Cannot use request_model together with body/query/path/headers")
        body = request_model

    # Use default adapter if none provided
    if adapter is None:
        adapter = PydanticAdapter(legacy_loc=legacy_loc)

    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        if _is_function_builder(func):
            warnings.warn(
                "@validate_http received an Azure Functions FunctionBuilder instead of "
                "your handler, which means it was applied ABOVE @app.route. Validation "
                "is NOT active for this function. Place @validate_http BELOW @app.route "
                "so it wraps the handler directly.",
                RuntimeWarning,
                stacklevel=2,
            )
            return func

        # Cross-repo decorator-order guard (azure-functions-logging#310).
        if _has_logging_metadata(func):
            warnings.warn(
                "@validate_http is applied ABOVE @with_context (from "
                "azure-functions-logging). In this order, validation error "
                "responses (e.g. 4xx) are produced before @with_context runs, so "
                "they are logged WITHOUT correlation context. Place @with_context "
                "ABOVE @validate_http (outermost, just under @app.route) so it "
                "wraps the validated handler.",
                RuntimeWarning,
                stacklevel=2,
            )

        is_async = inspect.iscoroutinefunction(func)

        func_sig = inspect.signature(func)
        func_params = func_sig.parameters

        request_param_name = _find_request_param(func, func_params)
        _validate_no_conflicts(func, request_param_name, body, query, path, headers, request_model)

        # Pre-build TypeAdapter for response_model at decoration time (#97)
        response_type_adapter = TypeAdapter(response_model) if response_model is not None else None

        config = PipelineConfig(
            body=body,
            query=query,
            path=path,
            headers=headers,
            request_model=request_model,
            response_model=response_model,
            adapter=adapter,
            error_formatter=error_formatter,
            func_params=func_params,
            request_param_name=request_param_name,
            response_type_adapter=response_type_adapter,
            success_status_code=status_code,
            handler_name=getattr(func, "__qualname__", None) or getattr(func, "__name__", None),
        )

        wrapper = _make_wrapper(func, config, is_async=is_async)
        return wrapper

    return decorator

Usage example: body + response validation

import azure.functions as func
from pydantic import BaseModel

from azure_functions_validation import validate_http


class CreateInvoiceBody(BaseModel):
    customer_id: str
    amount: float


class CreateInvoiceResponse(BaseModel):
    invoice_id: str
    status: str


app = func.FunctionApp()


@app.function_name(name="create_invoice")
@app.route(route="invoices", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=CreateInvoiceBody, response_model=CreateInvoiceResponse)
def create_invoice(req: func.HttpRequest, body: CreateInvoiceBody) -> CreateInvoiceResponse:
    return CreateInvoiceResponse(invoice_id="inv_1001", status="created")

Usage example: status codes and controlled errors

Use status_code= to set the success status (e.g. 201 for creation), and raise HttpError to return a controlled error through the standard {"detail": [...]} envelope without bypassing validation.

import azure.functions as func
from pydantic import BaseModel

from azure_functions_validation import HttpError, validate_http


class CreateUserBody(BaseModel):
    name: str


class UserResponse(BaseModel):
    id: int
    name: str


app = func.FunctionApp()

_USERS: dict[int, UserResponse] = {}


@app.function_name(name="create_user")
@app.route(route="users", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=CreateUserBody, response_model=UserResponse, status_code=201)
def create_user(req: func.HttpRequest, body: CreateUserBody) -> UserResponse:
    user = UserResponse(id=len(_USERS) + 1, name=body.name)
    _USERS[user.id] = user
    return user  # HTTP 201


@app.function_name(name="get_user")
@app.route(route="users/{user_id}", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(response_model=UserResponse)
def get_user(req: func.HttpRequest) -> UserResponse:
    user = _USERS.get(int(req.route_params["user_id"]))
    if user is None:
        raise HttpError(404, "User not found")
    return user

Usage example: query + path + headers

import azure.functions as func
from pydantic import BaseModel, ConfigDict, Field

from azure_functions_validation import validate_http


class UserQuery(BaseModel):
    include_deleted: bool = False


class UserPath(BaseModel):
    user_id: int = Field(ge=1)


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

    x_request_id: str = Field(alias="x-request-id")


app = func.FunctionApp()


@app.function_name(name="get_user")
@app.route(route="users/{user_id}", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(query=UserQuery, path=UserPath, headers=UserHeaders)
def get_user(
    req: func.HttpRequest,
    query: UserQuery,
    path: UserPath,
    headers: UserHeaders,
) -> dict[str, object]:
    return {
        "user_id": path.user_id,
        "include_deleted": query.include_deleted,
        "request_id": headers.x_request_id,
    }

Usage example: custom request_model shorthand

import azure.functions as func
from pydantic import BaseModel

from azure_functions_validation import validate_http


class CreateTaskRequest(BaseModel):
    title: str


app = func.FunctionApp()


@app.function_name(name="create_task")
@app.route(route="tasks", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(request_model=CreateTaskRequest)
def create_task(req: func.HttpRequest, req_model: CreateTaskRequest) -> dict[str, str]:
    return {"title": req_model.title}

Conflict rule

request_model cannot be combined with body, query, path, or headers. The decorator raises ValueError at import time if combined.

ResponseValidationError

Bases: Exception

Raised when response validation fails.

Initialize ResponseValidationError.

Parameters:

Name Type Description Default
message str

Error message

'Response validation error'
Source code in src/azure_functions_validation/errors.py
def __init__(self, message: str = "Response validation error"):
    """Initialize ResponseValidationError.

    Args:
        message: Error message
    """
    super().__init__(message)
    self.message = message

Usage example: handling response contract failures

import azure.functions as func
from pydantic import BaseModel

from azure_functions_validation import validate_http


class HealthResponse(BaseModel):
    status: str


app = func.FunctionApp()


@app.function_name(name="health")
@app.route(route="health", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(response_model=HealthResponse)
def health(req: func.HttpRequest) -> dict[str, str]:
    # Returning an invalid shape to show failure behavior
    return {"state": "ok"}

When response validation fails, the runtime returns HTTP 500 with this payload:

{
  "detail": [
    {
      "loc": ["response"],
      "msg": "Response validation failed",
      "type": "response_validation_error"
    }
  ]
}

HttpResponse bypass

Returning azure.functions.HttpResponse directly bypasses response model validation by design.

ErrorFormatter

Usage example: custom validation error shape

import azure.functions as func
from pydantic import BaseModel

from azure_functions_validation import ErrorFormatter, validate_http


class InputModel(BaseModel):
    value: int


def app_error_formatter(exc: Exception, status_code: int) -> dict[str, object]:
    return {
        "error": {
            "code": f"VALIDATION_{status_code}",
            "message": str(exc),
        }
    }


formatter: ErrorFormatter = app_error_formatter

app = func.FunctionApp()


@app.function_name(name="custom_error")
@app.route(route="custom_error", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=InputModel, error_formatter=formatter)
def custom_error(req: func.HttpRequest, body: InputModel) -> dict[str, int]:
    return {"value": body.value}

Formatter signature

Keep the formatter signature exactly (exc: Exception, status_code: int) -> dict[str, Any].

HttpError

Bases: Exception

Raised by a handler to return a controlled HTTP error response.

Rendered through the standard error envelope ({"detail": [...]}) by the validation pipeline, so controlled errors (e.g. 404, 409) share the same shape as automatic validation errors.

Parameters:

Name Type Description Default
status_code int

HTTP status code for the response (e.g. 404).

required
detail Any

Either a human-readable message (wrapped into a single {"loc": [], "msg": ..., "type": ...} entry) or a pre-built list of detail mappings matching the error-envelope schema.

'Error'
error_type str

type value used when detail is a plain message.

'http_error'
Source code in src/azure_functions_validation/errors.py
def __init__(
    self,
    status_code: int,
    detail: Any = "Error",
    *,
    error_type: str = "http_error",
) -> None:
    super().__init__(str(detail))
    self.status_code = status_code
    self.detail = detail
    self.error_type = error_type

to_detail()

Return the error-envelope detail list for this error.

Source code in src/azure_functions_validation/errors.py
def to_detail(self) -> list[dict[str, Any]]:
    """Return the error-envelope ``detail`` list for this error."""
    if isinstance(self.detail, list):
        return self.detail
    return [{"loc": [], "msg": str(self.detail), "type": self.error_type}]

Raise HttpError(status_code, detail) from a handler to return a controlled HTTP error rendered through the standard {"detail": [...]} envelope. detail may be a plain message (wrapped into a single entry) or a pre-built list of {"loc", "msg", "type"} mappings. Errors with status_code >= 500 are sanitized so internal details never leak to clients.

azure_functions_validation.testing.MockHttpRequest

A public test helper for unit-testing validated handlers. It subclasses the real azure.functions.HttpRequest, so it drives the genuine @validate_http pipeline end-to-end without a running Functions host.

from azure_functions_validation.testing import MockHttpRequest

request = MockHttpRequest(
    method="POST",
    json={"name": "Alice", "email": "alice@example.com"},
    params={"debug": "true"},
)
response = create_user(request)
assert response.status_code == 200

See Testing for the full list of constructor options.

Error response shape reference

Default validation and parsing errors use this envelope:

{
  "detail": [
    {
      "loc": ["body", "field_name"],
      "msg": "Field required",
      "type": "missing"
    }
  ],
  "error_format_version": 1
}

Stability contract

Every default error envelope carries a top-level error_format_version integer. It lets downstream consumers (frontends, API gateways) pin against a known schema and detect breaking changes explicitly instead of silently misparsing new fields.

  • The current version is 1.
  • The integer is bumped only when the default envelope changes in a backwards-incompatible way; additive, optional fields do not bump it.
  • The marker is present on all built-in envelopes — validation errors (4xx), sanitized server errors (5xx), and the internal-failure fallback.
  • A custom ErrorFormatter owns its output shape entirely: the marker is never injected into a successful custom formatter's response. Emit your own version field there if you need one. { "detail": [ { "loc": ["body", "field_name"], "msg": "Field required", "type": "missing" } ] } ```

Common status codes:

  • 400: invalid JSON parsing ("Invalid JSON").
  • 422: request validation failed.
  • 500: response validation failure or internal adapter failure.

Typical loc values

  • body errors: loc starts with "body"
  • query errors: loc starts with "query"
  • path errors: loc starts with "path"
  • header errors: loc starts with "headers"
  • response errors: loc equals ["response"]

Opting out of the source prefix

The leading source segment (body / query / path / headers) was added to disambiguate same-named fields across inputs. To keep the previous unprefixed loc for one migration cycle, pass legacy_loc=True to validate_http. This escape hatch will be removed in a future release. - response errors: loc equals ["response"]

Internal references

These modules are useful for advanced extension work but are internal APIs:

  • pipeline.py: PipelineConfig, run_pipeline, run_pipeline_async
  • adapter.py: ValidationAdapter, PydanticAdapter

For full implementation patterns, see Usage and Architecture.