Part of the Azure Functions Python DX Toolkit — dogfood-tested by azure-functions-cookbook-python.
Read this in: 한국어 | 日本語 | 简体中文
Validation and serialization for the Azure Functions Python v2 programming model.
Part of the Azure Functions Python DX Toolkit → Bring FastAPI-like developer experience to Azure Functions
Azure Functions Python v2 handlers often drift into the same repeated problems:
- Repeated manual parsing — every handler calls
req.get_json(),req.params.get(), handlesValueErrorindividually - Inconsistent error responses — some handlers return 400, others 422, formats vary across the project
- Missing response contracts — response payloads silently diverge from the intended schema
- No type safety — request data flows through as untyped dicts, bugs surface only at runtime
- Typed validation — body, query, path, and header parameters validated via Pydantic v2
- Automatic error responses — invalid requests get consistent
400/422JSON error bodies - Response model enforcement — mismatches raise
ResponseValidationError(HTTP 500) - Decorator-first API —
@validate_httpwraps your handler, no boilerplate needed - Custom error formatting — per-handler error shaping via
ErrorFormatter
@validate_http builds a validation pipeline once at import time, then runs it for every request:
flowchart LR
Client([HTTP Client]) --> AZ[Azure Functions wrapper]
AZ --> DEC["@validate_http"]
DEC --> PL[pipeline]
PL -->|parse / validate| AD[adapter]
AD --> H[your handler]
H --> PL2[pipeline]
PL2 -->|validate / serialize| AD2[adapter]
AD2 --> RESP([HttpResponse])
Decorator order and parameter passthrough:
@validate_httpmust sit directly beneath@app.route(...)so the Azure Functions worker still sees a bindable handler signature. WorkerCompat deliberately hides the validation-injected models from the worker-visible signature, so the worker binds only the request parameter under its actual name —reqby default, or your own@app.route(trigger_arg_name=...)name — plus any declared input/output binding params likecontext, by name;@validate_httpthen injects the validated models at runtime. The worker's name-binding contract — and this repo's WorkerCompat tests that verify it — is explained in How the worker binds handlers.
Without this package — manual parsing, manual errors, no contracts
import json
import azure.functions as func
app = func.FunctionApp()
@app.route(route="users", methods=["POST"])
def create_user(req: func.HttpRequest) -> func.HttpResponse:
try:
body = req.get_json()
except ValueError:
return func.HttpResponse(
json.dumps({"error": "Invalid JSON"}),
status_code=400,
mimetype="application/json",
)
name = body.get("name")
email = body.get("email")
if not name or not isinstance(name, str):
return func.HttpResponse(
json.dumps({"error": "name is required"}),
status_code=400,
mimetype="application/json",
)
if not email or not isinstance(email, str):
return func.HttpResponse(
json.dumps({"error": "email is required"}),
status_code=400,
mimetype="application/json",
)
return func.HttpResponse(
json.dumps({"message": f"Hello {name}", "status": "success"}),
mimetype="application/json",
)With @validate_http — typed, consistent, contract-enforced:
import azure.functions as func
from pydantic import BaseModel
from azure_functions_validation import validate_http
app = func.FunctionApp()
class CreateUserRequest(BaseModel):
name: str
email: str
class CreateUserResponse(BaseModel):
message: str
status: str = "success"
@app.route(route="users", methods=["POST"])
@validate_http(body=CreateUserRequest, response_model=CreateUserResponse)
def create_user(req: func.HttpRequest, body: CreateUserRequest) -> CreateUserResponse:
return CreateUserResponse(message=f"Hello {body.name}")Manual parsing and validation disappear from the handler. Error formatting and response contracts — handled.
Valid request → typed response:
$ curl -s -X POST http://localhost:7071/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'{"message": "Hello Alice", "status": "success"}HTTP 200
Missing required field → automatic error response:
$ curl -s -X POST http://localhost:7071/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'{
"detail": [
{
"loc": ["body", "email"],
"msg": "Field required",
"type": "missing"
}
],
"error_format_version": 1
}HTTP 422 — standardized error response, automatic
Invalid JSON → clear error:
$ curl -s -X POST http://localhost:7071/api/users \
-H "Content-Type: application/json" \
-d 'not json'{"detail": [{"loc": ["body"], "msg": "Invalid JSON", "type": "value_error"}], "error_format_version": 1}HTTP 400
You can. If a handler is simple, calling Model.model_validate(req.get_json()) inside a try/except is perfectly fine — this package does not replace Pydantic, it wraps your Pydantic models. Reach for @validate_http when the manual approach starts repeating across endpoints:
# Raw Pydantic in the handler — you own every step
def create_user(req: func.HttpRequest) -> func.HttpResponse:
try:
body = CreateUserRequest.model_validate(req.get_json())
except ValueError: # invalid JSON
return func.HttpResponse(..., status_code=400)
except ValidationError as exc: # shape/type errors
return func.HttpResponse(exc.json(), status_code=422) # your own envelope
result = CreateUserResponse(message=f"Hello {body.name}")
return func.HttpResponse(result.model_dump_json(), mimetype="application/json")The model is one line; the parsing, the two error branches, the error envelope, and the response serialization are the other twelve — and every handler re-implements them, often inconsistently. @validate_http collapses that to the decorator:
- No repeated glue — parsing,
try/except, and serialization move out of the handler body. - Consistent errors — every endpoint returns the same
400/422{"detail": [...]}envelope instead of a per-handler shape. - Response enforcement —
response_modelcatches contract drift on the way out, not just on the way in. - Discoverable metadata — the decorator records request/response models so
azure-functions-openapi's bridge can generate OpenAPI docs from the same models. See docs/usage.md for the metadata bridge.
In short: raw Pydantic validates one handler; @validate_http standardizes every handler. Compare the full Before / After above.
Choose native FunctionApp decorators when Functions bindings, worker-visible
signatures and the host's routing/auth model are your primary application model.
Choose FastAPI hosted by Azure Functions AsgiFunctionApp when you want FastAPI's
router, dependency injection, middleware and OpenAPI lifecycle as one ASGI app.
This library targets native HTTP handlers, not replacement validation inside a
FastAPI application. Host authentication remains host-owned in either approach.
| Feature | FastAPI | azure-functions-validation |
|---|---|---|
| Request body parsing | Built-in via type hints | @validate_http(body=Model) |
| Query/path/header validation | Query(), Path(), Header() |
@validate_http(query=Model, path=Model, headers=Model) |
| Response model | response_model= |
@validate_http(response_model=Model) |
| Validation errors | Automatic 422 | Automatic 422 with {"detail": [...]} |
| Error customization | Exception handlers | ErrorFormatter callback |
- Azure Functions Python v2 programming model
- HTTP-triggered functions registered on
func.FunctionApp() - Pydantic v2-based request and response validation
This package does not target the legacy function.json-based v1 programming model.
API documentation (azure-functions-openapi), runtime/graph deployment (azure-functions-langgraph), and project scaffolding (azure-functions-scaffold) are handled by sibling packages.
Three names cover three different contexts:
| Context | Name |
|---|---|
| GitHub repo | azure-functions-validation-python |
| PyPI package | azure-functions-validation |
| Python import | azure_functions_validation |
The repository carries the -python suffix to mark it as the Python implementation. The PyPI package follows Python ecosystem conventions and is published without the suffix, so installation stays idiomatic: pip install azure-functions-validation. See the FAQ entry for the long version.
pip install azure-functions-validationYour Azure Functions app should also include:
azure-functions
azure-functions-validation
For local development:
git clone https://github.com/yeongseon/azure-functions-validation-python.git
cd azure-functions-validation-python
pip install -e .[dev]import azure.functions as func
from pydantic import BaseModel
from azure_functions_validation import validate_http
class CreateUserRequest(BaseModel):
name: str
email: str
class CreateUserResponse(BaseModel):
message: str
status: str = "success"
app = func.FunctionApp()
@app.route(route="users", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=CreateUserRequest, response_model=CreateUserResponse)
def create_user(req: func.HttpRequest, body: CreateUserRequest) -> CreateUserResponse:
return CreateUserResponse(message=f"Hello {body.name}")Plain Pydantic models use lax coercion and ignore unknown fields. For public
JSON body contracts, consider a reusable
ConfigDict(strict=True, extra="forbid") base. Query, path, and header values
arrive as strings and usually need a separate model that keeps coercion. See
Strict API boundary models for runnable examples and
exact validation responses.
For a strict JSON endpoint, use
@validate_http(body=CreateUserRequest, response_model=CreateUserResponse, require_json_content_type=True).
The compatibility default is False: valid JSON is parsed even when labeled
text/plain or sent without Content-Type. Strict mode accepts application/json
and application/*+json; a non-empty body with another or missing media type
returns 415. An empty body still returns the missing-body 422, not 415.
See JSON Content-Type configuration.
Start the Functions host locally:
func startAfter deploying (see docs/deployment.md), the same request produces the same response in both environments.
curl -s http://localhost:7071/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'{"message": "Hello Alice", "status": "success"}curl -s "https://<your-app>.azurewebsites.net/api/users" \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'{"message": "Hello Alice", "status": "success"}Invalid requests return the same 400 error in both environments:
curl -s http://localhost:7071/api/users \
-H "Content-Type: application/json" \
-d 'not json'{"detail": [{"loc": ["body"], "msg": "Invalid JSON", "type": "value_error"}], "error_format_version": 1}HTTP 400
curl -s "https://<your-app>.azurewebsites.net/api/users" \
-H "Content-Type: application/json" \
-d 'not json'{"detail": [{"loc": ["body"], "msg": "Invalid JSON", "type": "value_error"}], "error_format_version": 1}HTTP 400
Manually verified by maintainers against a temporary Azure Functions deployment (koreacentral, Python 3.12, classic Linux Consumption plan — the plan in use at capture time). Response captured and URL anonymized. New deployments should use Flex Consumption: Linux Consumption retires on 30 September 2028 and receives no new Python versions. See docs/deployment.md for the verification status and context.
Return 201 on creation and raise controlled HTTP errors (e.g. 404) without
bypassing validation. Set the success status with status_code= and raise
HttpError to render an error through the standard {"detail": [...]} envelope:
import azure.functions as func
from pydantic import BaseModel
from azure_functions_validation import HttpError, validate_http
app = func.FunctionApp()
_USERS: dict[int, "UserResponse"] = {}
class CreateUserRequest(BaseModel):
name: str
email: str
class UserResponse(BaseModel):
id: int
name: str
@app.route(route="users", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@validate_http(body=CreateUserRequest, response_model=UserResponse, status_code=201)
def create_user(req: func.HttpRequest, body: CreateUserRequest) -> UserResponse:
user = UserResponse(id=len(_USERS) + 1, name=body.name)
_USERS[user.id] = user
return user # HTTP 201
@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") # standardized error envelope
return userA missing user returns a consistent error body:
{"detail": [{"loc": [], "msg": "User not found", "type": "http_error"}]}HTTP 404
HttpError also accepts a pre-built detail list for richer errors, and
server-side (>=500) errors are always sanitized so internal details never
leak to clients.
- You have HTTP-triggered Azure Functions that accept JSON request bodies
- You want Pydantic-based validation without writing manual parsing code
- You need consistent error response formats across handlers
- You want response schema enforcement to catch contract drift
- Project docs live under
docs/ - Strict request-model policies:
docs/strict-models.md - Smoke-tested examples live under
examples/ - Endpoint metadata contract:
docs/METADATA_SPEC.md— theendpointnamespace payload (parameters,inmapping,requiredrules) thatazure-functions-openapiconsumes - Product requirements:
PRD.md - Design principles:
DESIGN.md - Design notes:
docs/design/optional-body-347.md— why a configured body is always reported as required (#347)
This package is part of the Azure Functions Python DX Toolkit.
Design principle: azure-functions-validation owns request/response validation and serialization. azure-functions-openapi owns API documentation and spec generation. azure-functions-langgraph owns LangGraph runtime exposure.
| Package | Role |
|---|---|
| azure-functions-openapi-python | OpenAPI spec generation and Swagger UI |
| azure-functions-validation-python | Request/response validation and serialization |
| azure-functions-db-python | SQLAlchemy-powered DB integration helpers (poll-based pseudo trigger, input/output/client injection) |
| azure-functions-langgraph-python | LangGraph deployment adapter for Azure Functions |
| azure-functions-scaffold-python | Project scaffolding CLI |
| azure-functions-logging-python | Structured logging and observability |
| azure-functions-doctor-python | Pre-deploy diagnostic CLI |
| azure-functions-cookbook-python | Dogfood examples — runnable recipes that exercise the full toolkit |
When integrating with LLM-powered coding assistants, provide these files for context:
llms.txt— Concise index with quick start and API overviewllms-full.txt— Expanded reference with full signatures and patterns
Reference the files at repository root:
- https://github.com/yeongseon/azure-functions-validation-python/blob/main/llms.txt
- https://github.com/yeongseon/azure-functions-validation-python/blob/main/llms-full.txt
This project is an independent community project and is not affiliated with, endorsed by, or maintained by Microsoft.
Azure and Azure Functions are trademarks of Microsoft Corporation.
MIT