Skip to content

Fix Annotated forward ref resolution with from __future__ import annotations#15519

Open
MD-Mushfiqur123 wants to merge 9 commits into
fastapi:masterfrom
MD-Mushfiqur123:fix/annotated-forward-ref-lenient
Open

Fix Annotated forward ref resolution with from __future__ import annotations#15519
MD-Mushfiqur123 wants to merge 9 commits into
fastapi:masterfrom
MD-Mushfiqur123:fix/annotated-forward-ref-lenient

Conversation

@MD-Mushfiqur123
Copy link
Copy Markdown

Summary

Fixes #13056

When using from __future__ import annotations, annotations are stored as strings at compile time. If Annotated[SomeClass, Depends()] references a class defined after the route decorator, the forward reference could not be resolved at decoration time.

Root Cause

FastAPI's evaluate_forwardref() (wrapping Pydantic's try_eval_type) does not raise when a ForwardRef can't be resolved — it returns the raw ForwardRef object with success=False. The existing code only handled the string-to-ForwardRef conversion but never checked whether the resolution actually succeeded, allowing an unresolvable ForwardRef to leak into the type annotation. This caused Pydantic's TypeAdapter to fail with class-not-fully-defined and the Depends metadata to be lost.

Fix

When evaluate_forwardref() returns a ForwardRef (rather than the resolved type) and the annotation string contains Annotated[, a lenient fallback evaluates the annotation with a namespace that maps undefined names to Any. This preserves the Annotated structure so analyze_param can extract the Depends metadata.

Tests

  • test_forward_ref_annotated_depends — Validates that Annotated[Potato, Depends(get_potato)] works when Potato is defined after the route under from __future__ import annotations
  • test_forward_ref_annotated_depends_openapi — Validates that the OpenAPI schema doesn't expose the dependency as a query parameter

All existing 96+ dependency tests continue to pass.

…tations

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.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 15, 2026

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing MD-Mushfiqur123:fix/annotated-forward-ref-lenient (375b767) with master (b6abc93)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (6f2dbb9) during the generation of this report, so b6abc93 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.
@MD-Mushfiqur123 MD-Mushfiqur123 force-pushed the fix/annotated-forward-ref-lenient branch 2 times, most recently from 122db99 to 63613e0 Compare May 15, 2026 01:13
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.
@MD-Mushfiqur123 MD-Mushfiqur123 force-pushed the fix/annotated-forward-ref-lenient branch from 19693f9 to afd043b Compare May 15, 2026 01:19
@MD-Mushfiqur123 MD-Mushfiqur123 force-pushed the fix/annotated-forward-ref-lenient branch 2 times, most recently from 0815346 to e20b249 Compare May 15, 2026 01:37
@MD-Mushfiqur123 MD-Mushfiqur123 force-pushed the fix/annotated-forward-ref-lenient branch from 1ae49d4 to 477ebee Compare May 15, 2026 01:41
pre-commit-ci-lite Bot and others added 3 commits May 15, 2026 01:41
- 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)
…n 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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

📝 Docs preview

Last commit 375b767 at: https://7238a4d2.fastapitiangolo.pages.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can't use Annotated with ForwardRef

1 participant