From dc28d640db681b36acf35f93992f66b16b854798 Mon Sep 17 00:00:00 2001 From: BillionClaw Date: Tue, 17 Mar 2026 11:08:28 +0800 Subject: [PATCH] fix: resolve ForwardRef in Annotated types for OpenAPI schema generation When using `from __future__ import annotations` with `Annotated[Type, Depends(...)]`, ForwardRefs that couldn't be resolved at route registration time are now properly resolved when generating the OpenAPI schema. Fixes #13056 --- fastapi/_compat/v2.py | 29 +++++++++++++++-- tests/test_annotated_forward_ref.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/test_annotated_forward_ref.py diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 535af07849..7a66d210e5 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -152,6 +152,31 @@ class ModelField: config=self.config, ) + @property + def core_schema(self) -> CoreSchema: + """Get the core schema, rebuilding if necessary to resolve ForwardRefs.""" + schema = self._type_adapter.core_schema + # Check if this is a mock schema (unresolved ForwardRef) + if hasattr(schema, '_error_message'): + # Try to rebuild the type adapter to resolve ForwardRefs + # Search all loaded modules for the types we need + import sys + + # Build a combined namespace from all loaded modules + combined_globals: dict[str, Any] = {} + for module in sys.modules.values(): + if module is None: + continue + try: + module_dict = getattr(module, "__dict__", {}) + combined_globals.update(module_dict) + except Exception: + continue + + self._type_adapter.rebuild(force=True, _types_namespace=combined_globals) + schema = self._type_adapter.core_schema + return schema + def get_default(self) -> Any: if self.field_info.is_required(): return Undefined @@ -232,7 +257,7 @@ class ModelField: def _has_computed_fields(field: ModelField) -> bool: - computed_fields = field._type_adapter.core_schema.get("schema", {}).get( + computed_fields = field.core_schema.get("schema", {}).get( "computed_fields", [] ) return len(computed_fields) > 0 @@ -316,7 +341,7 @@ def get_definitions( if (separate_input_output_schemas or _has_computed_fields(field)) else "validation" ), - field._type_adapter.core_schema, + field.core_schema, ) for field in list(fields) + list(unique_flat_model_fields) ] diff --git a/tests/test_annotated_forward_ref.py b/tests/test_annotated_forward_ref.py new file mode 100644 index 0000000000..353353f868 --- /dev/null +++ b/tests/test_annotated_forward_ref.py @@ -0,0 +1,50 @@ +"""Test for ForwardRef resolution with Annotated and Depends. + +This test verifies the fix for: https://github.com/fastapi/fastapi/issues/13056 +When using `from __future__ import annotations`, ForwardRefs in Annotated types +should be properly resolved when generating OpenAPI schema. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated +import json + +from fastapi import Depends, FastAPI +from fastapi.openapi.utils import get_openapi + + +def test_annotated_with_forward_ref_openapi(): + """Test that Annotated with ForwardRef generates correct OpenAPI schema. + + This test reproduces the issue where Potato is defined AFTER the route, + which causes it to be a ForwardRef when the route is processed. + """ + 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 {'Hello': 'World'} + + # Define Potato AFTER the route - this is the problematic case + @dataclass + class Potato: + color: str + size: int + + # Generate OpenAPI schema - this should not raise an error + openapi_schema = get_openapi(title="Test", version="1.0.0", routes=app.routes) + + print(f"Schema keys: {openapi_schema.keys()}") + if "components" in openapi_schema: + print(json.dumps(openapi_schema["components"], indent=2)) + else: + print("No components in schema") + + +if __name__ == "__main__": + test_annotated_with_forward_ref_openapi() + print("Test completed!")