]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
⚡️ Avoid flattening dependencies for OpenAPI (#16076)
authorSebastián Ramírez <tiangolo@gmail.com>
Mon, 27 Jul 2026 17:16:40 +0000 (19:16 +0200)
committerGitHub <noreply@github.com>
Mon, 27 Jul 2026 17:16:40 +0000 (17:16 +0000)
fastapi/dependencies/models.py
fastapi/dependencies/utils.py
fastapi/openapi/utils.py
tests/test_dependency_models.py

index 58eae0eafb5555fe7e4742fc236eda8c84a518f9..5c7cbc82ba12e30d18887f170ca1704b59c8f915 100644 (file)
@@ -134,10 +134,6 @@ def _get_security_scheme(*, dependant: Dependant) -> SecurityBase:
     return unwrapped
 
 
-def _get_security_dependencies(*, dependant: Dependant) -> list[Dependant]:
-    return [dep for dep in dependant.dependencies if _is_security_scheme(dependant=dep)]
-
-
 @lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)
 def _is_gen_callable_cached(call_identity: _CallIdentity) -> bool:
     call = call_identity.call
index dedb394327a8b3338668292410ce08763e4a205d..01820e6a4cf477a22fffeaad1b308489d6a4a0a8 100644 (file)
@@ -144,81 +144,6 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
     )
 
 
-def get_flat_dependant(
-    dependant: Dependant,
-    *,
-    skip_repeats: bool = False,
-    visited: list[DependencyCacheKey] | None = None,
-    parent_oauth_scopes: list[str] | None = None,
-    _uses_scopes_cache: _UsesScopesCache | None = None,
-) -> Dependant:
-    track_visited = (
-        skip_repeats or visited is not None or _uses_scopes_cache is not None
-    )
-    if track_visited:
-        if visited is None:
-            visited = []
-        if _uses_scopes_cache is None:
-            _uses_scopes_cache = {}
-        visited.append(
-            _get_cache_key(
-                dependant=dependant,
-                uses_scopes_cache=_uses_scopes_cache,
-            )
-        )
-    use_parent_oauth_scopes = (parent_oauth_scopes or []) + (
-        _get_oauth_scopes(dependant=dependant)
-    )
-
-    flat_dependant = Dependant(
-        path_params=dependant.path_params.copy(),
-        query_params=dependant.query_params.copy(),
-        header_params=dependant.header_params.copy(),
-        cookie_params=dependant.cookie_params.copy(),
-        body_params=dependant.body_params.copy(),
-        name=dependant.name,
-        call=dependant.call,
-        request_param_name=dependant.request_param_name,
-        websocket_param_name=dependant.websocket_param_name,
-        http_connection_param_name=dependant.http_connection_param_name,
-        response_param_name=dependant.response_param_name,
-        background_tasks_param_name=dependant.background_tasks_param_name,
-        security_scopes_param_name=dependant.security_scopes_param_name,
-        own_oauth_scopes=dependant.own_oauth_scopes,
-        parent_oauth_scopes=use_parent_oauth_scopes,
-        use_cache=dependant.use_cache,
-        path=dependant.path,
-        scope=dependant.scope,
-    )
-    for sub_dependant in dependant.dependencies:
-        if skip_repeats:
-            assert visited is not None
-            if (
-                _get_cache_key(
-                    dependant=sub_dependant,
-                    uses_scopes_cache=_uses_scopes_cache,
-                )
-                in visited
-            ):
-                continue
-        flat_sub = get_flat_dependant(
-            sub_dependant,
-            skip_repeats=skip_repeats,
-            visited=visited,
-            parent_oauth_scopes=_get_oauth_scopes(dependant=flat_dependant),
-            _uses_scopes_cache=_uses_scopes_cache,
-        )
-        flat_dependant.dependencies.append(flat_sub)
-        flat_dependant.path_params.extend(flat_sub.path_params)
-        flat_dependant.query_params.extend(flat_sub.query_params)
-        flat_dependant.header_params.extend(flat_sub.header_params)
-        flat_dependant.cookie_params.extend(flat_sub.cookie_params)
-        flat_dependant.body_params.extend(flat_sub.body_params)
-        flat_dependant.dependencies.extend(flat_sub.dependencies)
-
-    return flat_dependant
-
-
 def _get_flat_body_params(dependant: Dependant) -> list[ModelField]:
     body_params: list[ModelField] = []
     dependants = [dependant]
index 392b2ed35e1743b7b311bdf368e3fa9c2583a928..74caac44a4187fa6646b5053dd5ca83641ee26df 100644 (file)
@@ -3,6 +3,7 @@ import http.client
 import inspect
 import warnings
 from collections.abc import Sequence
+from dataclasses import dataclass, field
 from typing import Any, Literal, cast
 
 from fastapi import routing
@@ -17,13 +18,14 @@ from fastapi._compat import (
 from fastapi.datastructures import DefaultPlaceholder, _Unset
 from fastapi.dependencies.models import (
     Dependant,
+    _get_cache_key,
     _get_oauth_scopes,
-    _get_security_dependencies,
     _get_security_scheme,
+    _is_security_scheme,
+    _UsesScopesCache,
 )
 from fastapi.dependencies.utils import (
     _get_flat_fields_from_params,
-    get_flat_dependant,
     get_flat_params,
     get_validation_alias,
 )
@@ -34,7 +36,7 @@ from fastapi.openapi.models import OpenAPI
 from fastapi.params import Body, ParamTypes
 from fastapi.responses import Response
 from fastapi.sse import _SSE_EVENT_SCHEMA
-from fastapi.types import ModelNameMap
+from fastapi.types import DependencyCacheKey, ModelNameMap
 from fastapi.utils import (
     deep_dict_update,
     generate_operation_id_for_path,
@@ -83,13 +85,57 @@ status_code_ranges: dict[str, str] = {
 }
 
 
-def get_openapi_security_definitions(
-    flat_dependant: Dependant,
+@dataclass
+class _OpenAPIDependencyData:
+    path_params: list[ModelField] = field(default_factory=list)
+    query_params: list[ModelField] = field(default_factory=list)
+    header_params: list[ModelField] = field(default_factory=list)
+    cookie_params: list[ModelField] = field(default_factory=list)
+    security_dependencies: list[tuple[Dependant, list[str]]] = field(
+        default_factory=list
+    )
+
+
+def _get_openapi_dependency_data(dependant: Dependant) -> _OpenAPIDependencyData:
+    dependency_data = _OpenAPIDependencyData()
+    visited: list[DependencyCacheKey] = []
+    uses_scopes_cache: _UsesScopesCache = {}
+    dependants: list[tuple[Dependant, list[str], bool]] = [(dependant, [], True)]
+    while dependants:
+        current_dependant, parent_oauth_scopes, is_root = dependants.pop()
+        cache_key = _get_cache_key(
+            dependant=current_dependant,
+            uses_scopes_cache=uses_scopes_cache,
+        )
+        if cache_key in visited:
+            continue
+        visited.append(cache_key)
+        dependency_data.path_params.extend(current_dependant.path_params)
+        dependency_data.query_params.extend(current_dependant.query_params)
+        dependency_data.header_params.extend(current_dependant.header_params)
+        dependency_data.cookie_params.extend(current_dependant.cookie_params)
+        oauth_scopes = parent_oauth_scopes.copy()
+        for scope in _get_oauth_scopes(dependant=current_dependant):
+            if scope not in oauth_scopes:
+                oauth_scopes.append(scope)
+        if not is_root and _is_security_scheme(dependant=current_dependant):
+            dependency_data.security_dependencies.append(
+                (current_dependant, oauth_scopes)
+            )
+        dependants.extend(
+            (sub_dependant, oauth_scopes, False)
+            for sub_dependant in reversed(current_dependant.dependencies)
+        )
+    return dependency_data
+
+
+def _get_openapi_security_definitions(
+    security_dependencies: list[tuple[Dependant, list[str]]],
 ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
     security_definitions = {}
     # Use a dict to merge scopes for same security scheme
     operation_security_dict: dict[str, list[str]] = {}
-    for security_dependency in _get_security_dependencies(dependant=flat_dependant):
+    for security_dependency, oauth_scopes in security_dependencies:
         security_scheme = _get_security_scheme(dependant=security_dependency)
         security_definition = jsonable_encoder(
             security_scheme.model,
@@ -101,7 +147,7 @@ def get_openapi_security_definitions(
         # Merge scopes for the same security scheme
         if security_name not in operation_security_dict:
             operation_security_dict[security_name] = []
-        for scope in _get_oauth_scopes(dependant=security_dependency):
+        for scope in oauth_scopes:
             if scope not in operation_security_dict[security_name]:
                 operation_security_dict[security_name].append(scope)
     operation_security = [
@@ -112,7 +158,7 @@ def get_openapi_security_definitions(
 
 def _get_openapi_operation_parameters(
     *,
-    flat_dependant: Dependant,
+    dependency_data: _OpenAPIDependencyData,
     model_name_map: ModelNameMap,
     field_mapping: dict[
         tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
@@ -120,10 +166,10 @@ def _get_openapi_operation_parameters(
     separate_input_output_schemas: bool = True,
 ) -> list[dict[str, Any]]:
     parameters = []
-    path_params = _get_flat_fields_from_params(flat_dependant.path_params)
-    query_params = _get_flat_fields_from_params(flat_dependant.query_params)
-    header_params = _get_flat_fields_from_params(flat_dependant.header_params)
-    cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
+    path_params = _get_flat_fields_from_params(dependency_data.path_params)
+    query_params = _get_flat_fields_from_params(dependency_data.query_params)
+    header_params = _get_flat_fields_from_params(dependency_data.header_params)
+    cookie_params = _get_flat_fields_from_params(dependency_data.cookie_params)
     parameter_groups = [
         (ParamTypes.path, path_params),
         (ParamTypes.query, query_params),
@@ -131,8 +177,8 @@ def _get_openapi_operation_parameters(
         (ParamTypes.cookie, cookie_params),
     ]
     default_convert_underscores = True
-    if len(flat_dependant.header_params) == 1:
-        first_field = flat_dependant.header_params[0]
+    if len(dependency_data.header_params) == 1:
+        first_field = dependency_data.header_params[0]
         if lenient_issubclass(first_field.field_info.annotation, BaseModel):
             default_convert_underscores = getattr(
                 first_field.field_info, "convert_underscores", True
@@ -283,14 +329,14 @@ def get_openapi_path(
     assert current_response_class, "A response class is needed to generate OpenAPI"
     route_response_media_type: str | None = current_response_class.media_type
     if route.include_in_schema:
-        flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
+        dependency_data = _get_openapi_dependency_data(route.dependant)
         all_route_params = [
             field
             for fields in (
-                flat_dependant.path_params,
-                flat_dependant.query_params,
-                flat_dependant.header_params,
-                flat_dependant.cookie_params,
+                dependency_data.path_params,
+                dependency_data.query_params,
+                dependency_data.header_params,
+                dependency_data.cookie_params,
             )
             for field in _get_flat_fields_from_params(fields)
         ]
@@ -299,15 +345,17 @@ def get_openapi_path(
                 route=route, method=method, operation_ids=operation_ids
             )
             parameters: list[dict[str, Any]] = []
-            security_definitions, operation_security = get_openapi_security_definitions(
-                flat_dependant=flat_dependant
+            security_definitions, operation_security = (
+                _get_openapi_security_definitions(
+                    security_dependencies=dependency_data.security_dependencies
+                )
             )
             if operation_security:
                 operation.setdefault("security", []).extend(operation_security)
             if security_definitions:
                 security_schemes.update(security_definitions)
             operation_parameters = _get_openapi_operation_parameters(
-                flat_dependant=flat_dependant,
+                dependency_data=dependency_data,
                 model_name_map=model_name_map,
                 field_mapping=field_mapping,
                 separate_input_output_schemas=separate_input_output_schemas,
index 9ba6009442cb6b98b2541110bb94985279a84a4a..2f857b3ecef2f3efbf6a9a12feede40ba8afa1e0 100644 (file)
@@ -6,7 +6,6 @@ from fastapi.dependencies.models import (
     _get_cache_key,
     _get_computed_scope,
     _get_oauth_scopes,
-    _get_security_dependencies,
     _get_security_scheme,
     _is_async_gen_callable,
     _is_async_gen_callable_cached,
@@ -146,7 +145,6 @@ def test_derived_values_are_not_stored_on_dependant() -> None:
     assert _get_oauth_scopes(dependant=dependant) == []
     assert not _uses_scopes(dependant=dependant, cache=uses_scopes_cache)
     assert not _uses_scopes(dependant=dependant, cache=uses_scopes_cache)
-    assert _get_security_dependencies(dependant=dependant) == []
     assert _get_computed_scope(dependant=dependant) is None
     assert _get_cache_key(dependant=dependant) == (async_dependency, (), "")
 
@@ -160,7 +158,6 @@ def test_security_scheme_helpers() -> None:
 
     assert _is_security_scheme(dependant=security_dependant)
     assert _get_security_scheme(dependant=security_dependant) is security_scheme
-    assert _get_security_dependencies(dependant=dependant) == [security_dependant]
     assert _uses_scopes(dependant=dependant)