Skip to content

fix: resolve ForwardRef inside Annotated in get_typed_annotation#15364

Open
OfekDanny wants to merge 1 commit into
fastapi:masterfrom
OfekDanny:fix/annotated-forwardref-resolution
Open

fix: resolve ForwardRef inside Annotated in get_typed_annotation#15364
OfekDanny wants to merge 1 commit into
fastapi:masterfrom
OfekDanny:fix/annotated-forwardref-resolution

Conversation

@OfekDanny
Copy link
Copy Markdown

Problem

Using a string literal as the first argument of Annotated causes FastAPI to treat the parameter as Any, producing an incorrect OpenAPI schema.

This affects two related cases:

Case 1 — explicit string in Annotated:

@app.post('/')
async def endpoint(potato: Annotated["Potato", Depends(get_potato)]):
    ...

Python automatically creates Annotated[ForwardRef("Potato"), Depends(get_potato)]. Because get_typed_annotation only resolved the annotation itself (if it was a top-level string), the ForwardRef inside Annotated was never resolved.

Case 2 — from __future__ import annotations with Annotated:

from __future__ import annotations

@app.post('/')
async def endpoint(potato: Annotated[Potato, Depends(get_potato)]):
    ...

With PEP 563, annotations are stored as strings. inspect.signature(call, eval_str=True) raises NameError (if Potato is defined later in the module), FastAPI falls back to the raw string, evaluate_forwardref produces Annotated[ForwardRef("Potato"), Depends(...)] — and the same unresolved ForwardRef problem occurs.

Result in both cases: potato appears as a plain query parameter with type any in the OpenAPI schema instead of being recognised as a dependency injection.

Closes #13056

Fix

Two small changes to get_typed_annotation:

  1. Handle ForwardRef inputs directly (not just str):

    if isinstance(annotation, str):
        annotation = ForwardRef(annotation)
    if isinstance(annotation, ForwardRef):   # was: inside the `if str` branch
        annotation = evaluate_forwardref(...)
  2. Recursively resolve ForwardRef inside Annotated's first argument:

    if get_origin(annotation) is Annotated:
        args = get_args(annotation)
        first_arg = args[0]
        if isinstance(first_arg, (str, ForwardRef)):
            resolved_first = get_typed_annotation(first_arg, globalns)
            annotation = Annotated[tuple([resolved_first] + list(args[1:]))]

All needed imports (Annotated, ForwardRef, get_args, get_origin) were already present.

Note: When Potato is defined after the route decorator in a from __future__ import annotations module, the ForwardRef still cannot be resolved at decorator time since Python executes the module top-to-bottom. A full fix for that pattern requires deferred/lazy annotation evaluation — tracked separately. This PR fixes the cases where the referenced type IS available at the time the route is registered.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 16, 2026

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing OfekDanny:fix/annotated-forwardref-resolution (dcce6ec) with master (3747204)1

Open in CodSpeed

Footnotes

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

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Apr 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

When a type annotation uses a string literal inside Annotated, e.g.
Annotated['Potato', Depends(get_potato)], Python stores it as
Annotated[ForwardRef('Potato'), Depends(get_potato)] at runtime.

get_typed_annotation only handled the case where the annotation itself
is a string/ForwardRef. It did not recurse into Annotated's first
argument. As a result, the ForwardRef was never resolved, FastAPI
could not identify the type, and the parameter was treated as Any,
producing an incorrect OpenAPI schema.

Fix by also handling the case where annotation is ForwardRef directly
(not wrapped in a string), and by recursively resolving any
ForwardRef found as the first argument of an Annotated type.

Fixes fastapi#13056
@OfekDanny OfekDanny force-pushed the fix/annotated-forwardref-resolution branch from 2cebfe0 to dcce6ec Compare April 24, 2026 10:41
@github-actions github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Apr 24, 2026
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