From 2703df6fdf7e3cb28229533260d13133cba8c749 Mon Sep 17 00:00:00 2001 From: giria660 Date: Thu, 26 Mar 2026 20:21:46 -0400 Subject: [PATCH 1/4] Fix Annotated ForwardRef dependencies with future annotations --- fastapi/dependencies/utils.py | 14 ++++- ...gified_annotation_forwardref_dependency.py | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/test_stringified_annotation_forwardref_dependency.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6b14dac8dc55e..e01e5d3f83ee2 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,6 +1,7 @@ import dataclasses import inspect import sys +from builtins import __dict__ as builtins_dict from collections.abc import ( AsyncGenerator, AsyncIterable, @@ -242,10 +243,21 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: return typed_signature +class _LenientTypeResolutionDict(dict[str, Any]): + def __missing__(self, key: str) -> Any: + value = builtins_dict.get(key, ForwardRef(key)) + self[key] = value + return value + + def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) - annotation = evaluate_forwardref(annotation, globalns, globalns) # ty: ignore[deprecated] + if isinstance(annotation, ForwardRef): + lenient_globalns = _LenientTypeResolutionDict(globalns) + annotation = evaluate_forwardref( # ty: ignore[deprecated] + annotation, lenient_globalns, lenient_globalns + ) if annotation is type(None): return None return annotation diff --git a/tests/test_stringified_annotation_forwardref_dependency.py b/tests/test_stringified_annotation_forwardref_dependency.py new file mode 100644 index 0000000000000..1778ad7a1833a --- /dev/null +++ b/tests/test_stringified_annotation_forwardref_dependency.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +app = FastAPI() + + +def get_potato() -> 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} + + +@dataclass +class Potato: + color: str + size: int + + +client = TestClient(app) + + +def test_stringified_annotated_forwardref_dependency(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"color": "red", "size": 10} + + +def test_stringified_annotated_forwardref_dependency_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.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 ed5ace87adfcbfb1f72d4c592771a1a0104fc1b2 Mon Sep 17 00:00:00 2001 From: giria660 Date: Thu, 26 Mar 2026 20:31:32 -0400 Subject: [PATCH 2/4] Fix mypy builtins lookup in annotation resolution --- fastapi/dependencies/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e01e5d3f83ee2..69b9ea18bc996 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,7 @@ import dataclasses import inspect import sys -from builtins import __dict__ as builtins_dict +import builtins from collections.abc import ( AsyncGenerator, AsyncIterable, @@ -245,7 +245,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: class _LenientTypeResolutionDict(dict[str, Any]): def __missing__(self, key: str) -> Any: - value = builtins_dict.get(key, ForwardRef(key)) + value = vars(builtins).get(key, ForwardRef(key)) self[key] = value return value From 87063e8046b9669fc7486c9e7eda4f9f5b2ad5b6 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, 27 Mar 2026 00:34:05 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 69b9ea18bc996..6162aa5efc625 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,7 @@ +import builtins import dataclasses import inspect import sys -import builtins from collections.abc import ( AsyncGenerator, AsyncIterable, From 380c4161e46474120473f7feaae996a7779c6389 Mon Sep 17 00:00:00 2001 From: giria660 Date: Mon, 30 Mar 2026 09:40:06 -0400 Subject: [PATCH 4/4] Use lenient ForwardRef resolution as fallback --- fastapi/dependencies/utils.py | 14 +++++++++----- tests/test_dependencies_utils.py | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6162aa5efc625..bc6cd85ac8d96 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -245,19 +245,23 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: class _LenientTypeResolutionDict(dict[str, Any]): def __missing__(self, key: str) -> Any: - value = vars(builtins).get(key, ForwardRef(key)) - self[key] = value - return value + return vars(builtins).get(key, ForwardRef(key)) def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) if isinstance(annotation, ForwardRef): - lenient_globalns = _LenientTypeResolutionDict(globalns) annotation = evaluate_forwardref( # ty: ignore[deprecated] - annotation, lenient_globalns, lenient_globalns + annotation, globalns, globalns ) + if isinstance(annotation, ForwardRef): + try: + annotation = eval( # noqa: S307 + annotation.__forward_arg__, _LenientTypeResolutionDict(globalns) + ) + except Exception: + pass if annotation is type(None): return None return annotation diff --git a/tests/test_dependencies_utils.py b/tests/test_dependencies_utils.py index 9257d1c9ee6cc..896042102ec96 100644 --- a/tests/test_dependencies_utils.py +++ b/tests/test_dependencies_utils.py @@ -1,3 +1,6 @@ +from typing import Annotated, ForwardRef, get_args, get_origin + +from fastapi import Depends, params from fastapi.dependencies.utils import get_typed_annotation @@ -6,3 +9,20 @@ def test_get_typed_annotation(): annotation = "None" typed_annotation = get_typed_annotation(annotation, globals()) assert typed_annotation is None + + +def test_get_typed_annotation_falls_back_to_lenient_forwardref_resolution(): + def dependency() -> None: + return None + + annotation = "Annotated[Potato, Depends(dependency)]" + typed_annotation = get_typed_annotation( + annotation, + {"Annotated": Annotated, "Depends": Depends, "dependency": dependency}, + ) + + assert get_origin(typed_annotation) is Annotated + type_annotation, depends = get_args(typed_annotation) + assert isinstance(type_annotation, ForwardRef) + assert type_annotation.__forward_arg__ == "Potato" + assert isinstance(depends, params.Depends)