← Back to Blog

When your framework validates before your library does

·5 min read

The FastAPI batch bug in pyRPC v0.14.0 is a case study in what happens when a framework's request validation sits upstream of a library's own validation. The two layers are solving different problems, but the first one can block the second.

Two layers of validation

In the FastAPI adapter, every request passes through two validation stages:

  • FastAPI validates first. The type annotation on the endpoint function tells FastAPI what the request body should look like. payload: dict[str, Any] means "this must be a JSON object." If it is not, FastAPI returns a 422 error before your code runs.
  • pyRPC validates second. handle_request parses the payload into an RpcRequest model, checks that the method exists, validates parameters against Pydantic TypeAdapters, and executes the procedure.

For single requests, this is harmless redundancy. FastAPI loosely checks "is it a dict?" then pyRPC does the real work. For batch requests, it is a hard block: FastAPI rejects the array before pyRPC ever sees it.

Why this pattern exists

FastAPI's type annotation system is one of its core features. You annotate a parameter, FastAPI parses and validates the incoming request against it, and you get automatic OpenAPI docs. This is idiomatic FastAPI — fighting it would be worse than working with it.

The problem is not that FastAPI validates. The problem is that the adapter declared a type that was too narrow for what the underlying library actually accepts.

How other frameworks handle this

Flask and Django do not have framework-level request body type validation. They hand you the raw body and you parse it yourself:

# Flask — no annotation, no gate
payload = request.get_json(force=True)
response = anyio.run(handle_request, payload, resolved)

# Django — raw parse, no framework validation
body = await request.body
payload = json.loads(body)
response = await handle_request(payload, router=resolved)

These adapters never block batch requests because they never ask the framework to validate the shape of the payload. pyRPC's own handle_request is the single validation boundary.

The lesson

When wrapping a library in a framework adapter, the adapter's type annotations should match the library's actual contract — not be a guess at what it might accept. A one-line annotation change from dict[str, Any] to dict[str, Any] | list[dict[str, Any]] fixed the batch bug while keeping FastAPI's docs and autocomplete accurate.

The alternative — payload: Any — would also work, but it throws away the OpenAPI documentation benefit that makes FastAPI adapters worth having in the first place.