Usage¶
This guide covers production patterns for @validate_http in the Azure Functions
Python v2 programming model.
If you are new to the package, start with Quickstart and then return here for deeper patterns.
Baseline Pattern¶
import azure.functions as func
from pydantic import BaseModel, Field
from azure_functions_validation import validate_http
class CreateUserBody(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str
class CreateUserResult(BaseModel):
user_id: int
message: str
app = func.FunctionApp()
@app.function_name(name="create_user")
@app.route(route="users", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=CreateUserBody, response_model=CreateUserResult)
def create_user(req: func.HttpRequest, body: CreateUserBody) -> CreateUserResult:
return CreateUserResult(user_id=1, message=f"Created {body.name}")
Mental model
Think of the decorator as a request/response contract layer: parse -> validate -> call handler -> validate response -> serialize.
Input source patterns¶
Body only¶
Use body=Model when your endpoint is driven only by JSON payload data.
@validate_http(body=CreateUserBody)
def handler(req: func.HttpRequest, body: CreateUserBody) -> dict[str, str]:
return {"name": body.name}
Query only¶
class ListQuery(BaseModel):
limit: int = Field(default=20, ge=1, le=100)
offset: int = Field(default=0, ge=0)
@validate_http(query=ListQuery)
def list_items(req: func.HttpRequest, query: ListQuery) -> dict[str, int]:
return {"limit": query.limit, "offset": query.offset}
Path only¶
class UserPath(BaseModel):
user_id: int = Field(ge=1)
@validate_http(path=UserPath)
def get_user(req: func.HttpRequest, path: UserPath) -> dict[str, int]:
return {"user_id": path.user_id}
Headers only¶
from pydantic import ConfigDict
class RequestHeaders(BaseModel):
model_config = ConfigDict(populate_by_name=True)
x_request_id: str = Field(alias="x-request-id")
@validate_http(headers=RequestHeaders)
def inspect_headers(req: func.HttpRequest, headers: RequestHeaders) -> dict[str, str]:
return {"request_id": headers.x_request_id}
Header aliases
Header keys usually contain hyphens. Use Field(alias="x-header-name")
plus ConfigDict(populate_by_name=True) when your Python attribute uses
underscores.
Combining body + query + headers¶
This is a common production shape for list/search endpoints.
import azure.functions as func
from pydantic import BaseModel, ConfigDict, Field
from azure_functions_validation import validate_http
class SearchBody(BaseModel):
terms: list[str]
class SearchQuery(BaseModel):
page: int = Field(default=1, ge=1)
page_size: int = Field(default=20, ge=1, le=100)
class SearchHeaders(BaseModel):
model_config = ConfigDict(populate_by_name=True)
x_request_id: str = Field(alias="x-request-id")
class SearchResponse(BaseModel):
request_id: str
page: int
page_size: int
count: int
items: list[str]
app = func.FunctionApp()
@app.function_name(name="search")
@app.route(route="search", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(
body=SearchBody,
query=SearchQuery,
headers=SearchHeaders,
response_model=SearchResponse,
)
def search(
req: func.HttpRequest,
body: SearchBody,
query: SearchQuery,
headers: SearchHeaders,
) -> SearchResponse:
items = [term.upper() for term in body.terms]
return SearchResponse(
request_id=headers.x_request_id,
page=query.page,
page_size=query.page_size,
count=len(items),
items=items,
)
When to combine
Use combined validation when each source has distinct semantics: body for domain payload, query for paging/filtering, headers for metadata.
request_model shorthand¶
request_model is shorthand for body validation and injects req_model.
class CreateTaskBody(BaseModel):
title: str
@validate_http(request_model=CreateTaskBody)
def create_task(req: func.HttpRequest, req_model: CreateTaskBody) -> dict[str, str]:
return {"title": req_model.title}
Non-combinable shorthand
Do not combine request_model with body, query, path, or headers.
The decorator rejects this configuration at import time.
Response validation patterns¶
Pattern A: model instance return¶
class PingResponse(BaseModel):
status: str
@validate_http(response_model=PingResponse)
def ping(req: func.HttpRequest) -> PingResponse:
return PingResponse(status="ok")
Pattern B: dict return validated against model¶
class HealthResponse(BaseModel):
name: str
status: str
@validate_http(response_model=HealthResponse)
def health(req: func.HttpRequest) -> dict[str, str]:
return {"name": "api", "status": "ready"}
Pattern C: list response model¶
class ItemOut(BaseModel):
id: int
name: str
@validate_http(response_model=list[ItemOut])
def list_items(req: func.HttpRequest) -> list[dict[str, object]]:
return [{"id": 1, "name": "alpha"}, {"id": 2, "name": "beta"}]
Pattern D: bypass with HttpResponse¶
@validate_http(response_model=HealthResponse)
def custom_status(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(status_code=204)
Bypass behavior
Returning func.HttpResponse skips response model validation intentionally.
Error handling strategies¶
Default strategy: standardized envelope¶
Without custom formatting, parsing/validation errors are emitted as:
{
"detail": [
{
"loc": ["query", "limit"],
"msg": "Input should be less than or equal to 100",
"type": "less_than_equal"
}
]
}
Custom strategy: per-handler error_formatter¶
from typing import Any
def formatter(exc: Exception, status_code: int) -> dict[str, Any]:
return {
"error": {
"code": f"VALIDATION_{status_code}",
"message": str(exc),
}
}
@validate_http(body=CreateUserBody, error_formatter=formatter)
def create_user_custom(req: func.HttpRequest, body: CreateUserBody) -> dict[str, str]:
return {"name": body.name}
Strategy guidance¶
- Keep one default format for most handlers.
- Use custom formatters only when external contracts require it.
- Include status-coded machine fields in custom payloads.
- Avoid exposing internal implementation details in
500errors.
Formatter scope
Formatters apply per handler; there is no global registry in this package.
Async handlers¶
@validate_http supports async def handlers directly.
import asyncio
class AsyncBody(BaseModel):
name: str
class AsyncResult(BaseModel):
message: str
@validate_http(body=AsyncBody, response_model=AsyncResult)
async def async_hello(req: func.HttpRequest, body: AsyncBody) -> AsyncResult:
await asyncio.sleep(0)
return AsyncResult(message=f"Hello {body.name}")
Testing validated handlers¶
You can unit-test decorated handlers by constructing a mocked HttpRequest.
import json
from unittest.mock import Mock
from azure.functions import HttpRequest
def make_request(body: bytes, params: dict[str, str] | None = None) -> Mock:
req = Mock(spec=HttpRequest)
req.method = "POST"
req.url = "http://localhost"
req.get_body.return_value = body
req.params = params or {}
req.route_params = {}
req.headers = {}
return req
def test_create_user_success() -> None:
response = create_user(make_request(b'{"name": "Ada", "email": "ada@example.com"}'))
payload = json.loads(response.get_body().decode())
assert response.status_code == 200
assert payload["message"] == "Created Ada"
def test_create_user_validation_error() -> None:
response = create_user(make_request(b'{"name": "", "email": "bad"}'))
payload = json.loads(response.get_body().decode())
assert response.status_code == 422
assert "detail" in payload
Test level
Use unit tests for validation behavior and integration tests for full route wiring inside an Azure Functions host.
Integration with azure-functions-openapi¶
azure-functions-validation and azure-functions-openapi are complementary:
- this package validates runtime requests/responses
- OpenAPI tooling generates API documentation
- both share the same Pydantic models
from pydantic import BaseModel
class CreateOrderBody(BaseModel):
item_id: str
quantity: int
class CreateOrderResponse(BaseModel):
order_id: str
status: str
# Runtime contract
@validate_http(body=CreateOrderBody, response_model=CreateOrderResponse)
def create_order(req: func.HttpRequest, body: CreateOrderBody) -> CreateOrderResponse:
return CreateOrderResponse(order_id="ord_1", status="created")
# Documentation tooling can consume these model schemas:
# CreateOrderBody.model_json_schema()
# CreateOrderResponse.model_json_schema()
Single source of truth
Define schema constraints once in Pydantic models and reuse them for runtime validation and generated API docs.
Common gotchas¶
- Empty request body with
body=configured returns422. - Invalid JSON syntax returns
400. - Response shape mismatch with
response_model=returns500. - First positional parameter must be the request object.
- Naming the first positional parameter
body/query/path/headerscan conflict.
See Troubleshooting for issue-by-issue fixes.