Browse Source
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.
2 changed files with 99 additions and 1 deletions
@ -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": {}}}, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
Loading…
Reference in new issue