From becafff6b5ca215238d1b9ede567648db2d2084f Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 06:52:19 +0600 Subject: [PATCH] Fix Annotated forward ref resolution with from __future__ import annotations When using rom __future__ import annotations, annotations are stored as strings. If Annotated[SomeClass, Depends()] references a class defined after the route decorator, the forward reference could not be resolved at decoration time. FastAPI's evaluate_forwardref() does not raise on unresolvable references - it returns the raw ForwardRef object. The existing code only handled the string-to-ForwardRef conversion but did not check whether resolution actually succeeded, allowing an unresolvable ForwardRef to leak into the type annotation. This caused Pydantic to fail with 'class-not-fully-defined' errors and the Depends metadata to be lost. The fix adds a fallback: when evaluate_forwardref() returns a ForwardRef (rather than the resolved type) and the annotation string is Annotated- shaped, a lenient resolution evaluates the annotation with a namespace that maps undefined names to Any. This preserves the Annotated structure so FastAPI can extract the Depends metadata. --- fastapi/dependencies/utils.py | 33 +++++++++++++- tests/test_annotated_forward_ref.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/test_annotated_forward_ref.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7c6558c695..b8f68fcabe 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -246,11 +246,42 @@ def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) annotation = evaluate_forwardref(annotation, globalns, globalns) - if annotation is type(None): + if isinstance(annotation, ForwardRef): + if "Annotated[" in annotation.__forward_arg__: + annotation = _resolve_forward_ref_lenient( + annotation, globalns + ) + elif annotation is type(None): return None return annotation +def _resolve_forward_ref_lenient( + fwd_ref: ForwardRef, + globalns: dict[str, Any], +) -> Any: + class _LenientNamespace(dict): + def __missing__(self, key: str) -> Any: + return Any + + localns = _LenientNamespace(globalns) + result = fwd_ref._evaluate(globalns, localns, set()) + return result + + +def _resolve_forward_ref_lenient( + fwd_ref: ForwardRef, + globalns: dict[str, Any], +) -> Any: + class _LenientNamespace(dict): + def __missing__(self, key: str) -> Any: + return Any + + localns = _LenientNamespace(globalns) + result = fwd_ref._evaluate(globalns, localns, set()) + return result + + def get_typed_return_annotation(call: Callable[..., Any]) -> Any: signature = _get_signature(call) unwrapped = inspect.unwrap(call) diff --git a/tests/test_annotated_forward_ref.py b/tests/test_annotated_forward_ref.py new file mode 100644 index 0000000000..9074207afe --- /dev/null +++ b/tests/test_annotated_forward_ref.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import Annotated + +from inline_snapshot import snapshot + +from fastapi import Depends, FastAPI + +# Simulate the real-world bug: Potato is defined AFTER the route decorator +# under `from __future__ import annotations`. + +app = FastAPI() + + +def get_potato(): + return Potato(color="red", size=10) + + +@app.get("/") +async def read_root(potato: Annotated[Potato, Depends(get_potato)]): + return {"color": potato.color, "size": potato.size} + + +from dataclasses import dataclass + + +@dataclass +class Potato: + color: str + size: int + + +def test_forward_ref_annotated_depends(): + from fastapi.testclient import TestClient + + client = TestClient(app) + resp = client.get("/") + assert resp.status_code == 200, resp.text + assert resp.json() == {"color": "red", "size": 10} + + +def test_forward_ref_annotated_depends_openapi(): + from fastapi.testclient import TestClient + + client = TestClient(app) + resp = client.get("/openapi.json") + assert resp.status_code == 200, resp.text + assert resp.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } + )