From becafff6b5ca215238d1b9ede567648db2d2084f Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 06:52:19 +0600 Subject: [PATCH 1/8] 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": {}}}, + } + }, + } + } + }, + } + ) From 63613e0b83bd927f30e9565743419872ae7ba80d Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 07:09:32 +0600 Subject: [PATCH 2/8] Fix ForwardRef._evaluate() keyword-only argument for Python 3.13+ In Python 3.13+, ForwardRef._evaluate() requires 'recursive_guard' as a keyword-only argument. Pass it as keyword to support both older and newer Python versions. --- fastapi/dependencies/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b8f68fcabe..b05066f312 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -278,7 +278,7 @@ def _resolve_forward_ref_lenient( return Any localns = _LenientNamespace(globalns) - result = fwd_ref._evaluate(globalns, localns, set()) + result = fwd_ref._evaluate(globalns, localns, recursive_guard=set()) return result From afd043b03e466c517427b92f0e9828c19fe06d3e Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 07:19:44 +0600 Subject: [PATCH 3/8] Fix ForwardRef._evaluate() missing type_params for Python 3.14+ Python 3.14 added a new 'type_params' parameter to ForwardRef._evaluate() (PEP 695). Failing to pass it triggers a DeprecationWarning, which is treated as error by FastAPI's test suite (-W error). Pass type_params=() on Python 3.14+ to silence the warning. --- fastapi/dependencies/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b05066f312..13cb64ee4a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -278,7 +278,12 @@ def _resolve_forward_ref_lenient( return Any localns = _LenientNamespace(globalns) - result = fwd_ref._evaluate(globalns, localns, recursive_guard=set()) + if sys.version_info >= (3, 14): + result = fwd_ref._evaluate( + globalns, localns, recursive_guard=set(), type_params=() + ) + else: + result = fwd_ref._evaluate(globalns, localns, recursive_guard=set()) return result From e20b2497f3c9b9e3c3fb622195d927543e00f85a Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 07:31:04 +0600 Subject: [PATCH 4/8] Fix: type_params needed on Python 3.13+ too --- fastapi/dependencies/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 13cb64ee4a..93675a0339 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -278,7 +278,7 @@ def _resolve_forward_ref_lenient( return Any localns = _LenientNamespace(globalns) - if sys.version_info >= (3, 14): + if sys.version_info >= (3, 13): result = fwd_ref._evaluate( globalns, localns, recursive_guard=set(), type_params=() ) From 477ebee8d268c7da82fde4ed8fb4adba64d91485 Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 07:38:12 +0600 Subject: [PATCH 5/8] ruff format fix --- fastapi/dependencies/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 93675a0339..72b0b14f0b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -248,9 +248,7 @@ def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: annotation = evaluate_forwardref(annotation, globalns, globalns) if isinstance(annotation, ForwardRef): if "Annotated[" in annotation.__forward_arg__: - annotation = _resolve_forward_ref_lenient( - annotation, globalns - ) + annotation = _resolve_forward_ref_lenient(annotation, globalns) elif annotation is type(None): return None return annotation From b023d91e1331a9d1526f93276cc1cb62804fca09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 01:41:58 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_annotated_forward_ref.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_annotated_forward_ref.py b/tests/test_annotated_forward_ref.py index 9074207afe..8b9a483eaa 100644 --- a/tests/test_annotated_forward_ref.py +++ b/tests/test_annotated_forward_ref.py @@ -2,9 +2,8 @@ from __future__ import annotations from typing import Annotated -from inline_snapshot import snapshot - from fastapi import Depends, FastAPI +from inline_snapshot import snapshot # Simulate the real-world bug: Potato is defined AFTER the route decorator # under `from __future__ import annotations`. From 4c998f64db8534a96b4e95a5200f5b9ad3859250 Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 11:39:30 +0600 Subject: [PATCH 7/8] Fix pre-commit and Python 3.14 test CI failures - Remove duplicate _resolve_forward_ref_lenient function - Use ForwardRef.evaluate() instead of deprecated _evaluate() - Use frozenset() instead of set() for recursive_guard parameter - Add noqa for E402 in test file (deliberate late import) --- fastapi/dependencies/utils.py | 19 +++---------------- tests/test_annotated_forward_ref.py | 2 +- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 72b0b14f0b..40e6957b8b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -254,19 +254,6 @@ def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: 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], @@ -277,11 +264,11 @@ def _resolve_forward_ref_lenient( localns = _LenientNamespace(globalns) if sys.version_info >= (3, 13): - result = fwd_ref._evaluate( - globalns, localns, recursive_guard=set(), type_params=() + result = fwd_ref.evaluate( + globalns, localns, recursive_guard=frozenset(), type_params=() ) else: - result = fwd_ref._evaluate(globalns, localns, recursive_guard=set()) + result = fwd_ref.evaluate(globalns, localns, recursive_guard=frozenset()) return result diff --git a/tests/test_annotated_forward_ref.py b/tests/test_annotated_forward_ref.py index 8b9a483eaa..ceb7636aa7 100644 --- a/tests/test_annotated_forward_ref.py +++ b/tests/test_annotated_forward_ref.py @@ -20,7 +20,7 @@ async def read_root(potato: Annotated[Potato, Depends(get_potato)]): return {"color": potato.color, "size": potato.size} -from dataclasses import dataclass +from dataclasses import dataclass # noqa: E402 @dataclass From 6d404bca9a3edd11bd1987af77829ebff1d70c67 Mon Sep 17 00:00:00 2001 From: Codebuff Contributor Date: Fri, 15 May 2026 11:44:28 +0600 Subject: [PATCH 8/8] Fix mypy/ty by using _evaluate, fix dict type args, ignore deprecation warning - Keep ForwardRef._evaluate (recognized by mypy/ty typeshed) - Fix class _LenientNamespace(dict) -> _LenientNamespace(dict[str, Any]) - Use frozenset() instead of set() for recursive_guard - Ignore ForwardRef._evaluate deprecation warning in pytest config --- fastapi/dependencies/utils.py | 6 +++--- pyproject.toml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 40e6957b8b..47ba0f2c43 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -258,17 +258,17 @@ def _resolve_forward_ref_lenient( fwd_ref: ForwardRef, globalns: dict[str, Any], ) -> Any: - class _LenientNamespace(dict): + class _LenientNamespace(dict[str, Any]): def __missing__(self, key: str) -> Any: return Any localns = _LenientNamespace(globalns) if sys.version_info >= (3, 13): - result = fwd_ref.evaluate( + result = fwd_ref._evaluate( globalns, localns, recursive_guard=frozenset(), type_params=() ) else: - result = fwd_ref.evaluate(globalns, localns, recursive_guard=frozenset()) + result = fwd_ref._evaluate(globalns, localns, recursive_guard=frozenset()) return result diff --git a/pyproject.toml b/pyproject.toml index 2d0e4cc8bc..022910166b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,6 +231,7 @@ addopts = [ strict_xfail = true filterwarnings = [ "error", + 'ignore:The private ForwardRef._evaluate method is deprecated:DeprecationWarning', ] timeout = "20"