Skip to main content

Azure Functions Validation

Part of the Azure Functions Python DX Toolkit — dogfood-tested by azure-functions-cookbook-python.

PyPI Downloads Python Version CI Release Security Scans codecov pre-commit Docs License: MIT

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

Python 3.10 is deprecated. Support ends in the next minor release — Python 3.10 reaches end of life in October 2026. Importing the package on Python 3.10 emits a FutureWarning; upgrade to Python 3.11 or newer.

Why this exists

Azure Functions Python v2 handlers often drift into the same repeated problems:

  • Repeated manual parsing — every handler calls req.get_json(), req.params.get(), handles ValueError individually
  • 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

What it does

  • Typed validation — body, query, path, and header parameters validated via Pydantic v2
  • Automatic error responses — invalid requests get consistent 400/422 JSON error bodies
  • Response model enforcement — mismatches raise ResponseValidationError (HTTP 500)
  • Decorator-first API — @validate_http wraps your handler, no boilerplate needed
  • Custom error formatting — per-handler error shaping via ErrorFormatter

How it works

@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_http must 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 req (plus any declared input/output binding params like context) by name; @validate_http then 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.

Before / After

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.

What you get

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"
    }
  ]
}

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"}]}

HTTP 400

Why not just use Pydantic directly?

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_model catches 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.

FastAPI comparison

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

Scope

  • 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.

What this package does not do

API documentation (azure-functions-openapi), runtime/graph deployment (azure-functions-langgraph), and project scaffolding (azure-functions-scaffold) are handled by sibling packages.

Package names

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.

Installation

pip install azure-functions-validation

Your 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]

Quick Start

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}")

Start the Functions host locally:

func start

Verify locally and on Azure

After deploying (see docs/deployment.md), the same request produces the same response in both environments.

Local

curl -s http://localhost:7071/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'
{"message": "Hello Alice", "status": "success"}

Azure

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:

Local

curl -s http://localhost:7071/api/users \
  -H "Content-Type: application/json" \
  -d 'not json'
{"detail": [{"loc": ["body"], "msg": "Invalid JSON", "type": "value_error"}]}

HTTP 400

Azure

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"}]}

HTTP 400

Manually verified by maintainers against a temporary Azure Functions deployment (koreacentral, Python 3.12, Consumption plan); response captured and URL anonymized. See docs/deployment.md for the verification status and context.

Status codes and controlled errors

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 user

A 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.

When to use

  • 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

Documentation

  • Project docs live under docs/
  • Smoke-tested examples live under examples/
  • Endpoint metadata contract: docs/METADATA_SPEC.md — the endpoint namespace payload (parameters, in mapping, required rules) that azure-functions-openapi consumes
  • 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)

Ecosystem

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

For AI Coding Assistants

When integrating with LLM-powered coding assistants, provide these files for context:

  • llms.txt — Concise index with quick start and API overview
  • llms-full.txt — Expanded reference with full signatures and patterns

Reference the files at repository root:

Disclaimer

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.

License

MIT

Metadata

Release files for azure-functions-validation 0.13.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for azure-functions-validation 0.13.2
File Size Uploaded
azure_functions_validation-0.13.2.tar.gz 183.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for azure-functions-validation 0.13.2
File Interpreter ABI Platform
azure_functions_validation-0.13.2-py3-none-any.whl Python 3 none any Details

Total release size: 219.0 kB

Release files / azure_functions_validation-0.13.2.tar.gz

Download URL azure_functions_validation-0.13.2.tar.gz
Size 183.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b8663758d06ee3c1bb9bf0df59130032d86c8c36acf5467f84c9805fea576c8a
BLAKE2b-256 checksum
How to use checksums
91196ef58aad7b422cbb06c61b1971754cbe6b4ccbfbc685266b4d780b6b80a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / azure_functions_validation-0.13.2-py3-none-any.whl

Download URL azure_functions_validation-0.13.2-py3-none-any.whl
Size 35.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b1f0fdd6df43094fe564f61dc15a720e935f4a1ed24cd46ee2883c56110bf91a
BLAKE2b-256 checksum
How to use checksums
f12e874d9d68d490a8d59dbeb0a81a2d08974674bfc272b2374a553e1703b3e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.13.2 This release

2 release files

0.12.0

2 release files

0.11.2

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.1

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.7

2 release files

0.7.6

2 release files

0.7.5

2 release files

0.7.4

2 release files

0.7.3

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.7

2 release files

0.5.6

2 release files

0.5.5

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page