diff --git a/README.md b/README.md
index 4fd87298e..09cd38da1 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ The key features are:
+
diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml
index 7a015e404..943b92adb 100644
--- a/docs/en/data/sponsors.yml
+++ b/docs/en/data/sponsors.yml
@@ -26,6 +26,9 @@ gold:
- url: https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi
title: Deploy enterprise applications at startup speed
img: https://fastapi.tiangolo.com/img/sponsors/railway.png
+ - url: https://serpapi.com/?utm_source=fastapi_website
+ title: "SerpApi: Web Search API"
+ img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png
silver:
- url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display
title: Pay as you go for market data
diff --git a/docs/en/docs/img/sponsors/serpapi-banner.png b/docs/en/docs/img/sponsors/serpapi-banner.png
new file mode 100644
index 000000000..3c3fd629e
Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi-banner.png differ
diff --git a/docs/en/docs/img/sponsors/serpapi.png b/docs/en/docs/img/sponsors/serpapi.png
new file mode 100644
index 000000000..d7258ef70
Binary files /dev/null and b/docs/en/docs/img/sponsors/serpapi.png differ
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index fb4c26701..bbcbc74b7 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -7,8 +7,20 @@ hide:
## Latest Changes
+### Refactors
+
+* ♻️ Refactor internals of dependencies, simplify code and remove `get_param_sub_dependant`. PR [#14255](https://github.com/fastapi/fastapi/pull/14255) by [@tiangolo](https://github.com/tiangolo).
+* ♻️ Refactor internals of dependencies, simplify using dataclasses. PR [#14254](https://github.com/fastapi/fastapi/pull/14254) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.120.2
+
+### Fixes
+
+* 🐛 Fix separation of schemas with nested models introduced in 0.119.0. PR [#14246](https://github.com/fastapi/fastapi/pull/14246) by [@tiangolo](https://github.com/tiangolo).
+
### Internal
+* 🔧 Add sponsor: SerpApi. PR [#14248](https://github.com/fastapi/fastapi/pull/14248) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump actions/download-artifact from 5 to 6. PR [#14236](https://github.com/fastapi/fastapi/pull/14236) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#14237](https://github.com/fastapi/fastapi/pull/14237) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#14235](https://github.com/fastapi/fastapi/pull/14235) by [@dependabot[bot]](https://github.com/apps/dependabot).
diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html
index c7ffaef5d..be31bd75c 100644
--- a/docs/en/overrides/main.html
+++ b/docs/en/overrides/main.html
@@ -80,6 +80,12 @@
+
{% endblock %}
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 787c52dda..a4c17a6bd 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.120.1"
+__version__ = "0.120.2"
from starlette import status as status
diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index fb2c691d8..6a87b9ae9 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -207,11 +207,31 @@ def get_definitions(
override_mode: Union[Literal["validation"], None] = (
None if separate_input_output_schemas else "validation"
)
- flat_models = get_flat_models_from_fields(fields, known_models=set())
- flat_model_fields = [
- ModelField(field_info=FieldInfo(annotation=model), name=model.__name__)
- for model in flat_models
+ validation_fields = [field for field in fields if field.mode == "validation"]
+ serialization_fields = [field for field in fields if field.mode == "serialization"]
+ flat_validation_models = get_flat_models_from_fields(
+ validation_fields, known_models=set()
+ )
+ flat_serialization_models = get_flat_models_from_fields(
+ serialization_fields, known_models=set()
+ )
+ flat_validation_model_fields = [
+ ModelField(
+ field_info=FieldInfo(annotation=model),
+ name=model.__name__,
+ mode="validation",
+ )
+ for model in flat_validation_models
+ ]
+ flat_serialization_model_fields = [
+ ModelField(
+ field_info=FieldInfo(annotation=model),
+ name=model.__name__,
+ mode="serialization",
+ )
+ for model in flat_serialization_models
]
+ flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields
input_types = {f.type_ for f in fields}
unique_flat_model_fields = {
f for f in flat_model_fields if f.type_ not in input_types
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index aa06dd2a9..18f6a234e 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -125,23 +125,6 @@ def ensure_multipart_is_installed() -> None:
raise RuntimeError(multipart_not_installed_error) from None
-def get_param_sub_dependant(
- *,
- param_name: str,
- depends: params.Depends,
- path: str,
- security_scopes: Optional[List[str]] = None,
-) -> Dependant:
- assert depends.dependency
- return get_sub_dependant(
- depends=depends,
- dependency=depends.dependency,
- path=path,
- name=param_name,
- security_scopes=security_scopes,
- )
-
-
def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
assert callable(depends.dependency), (
"A parameter-less dependency must have a callable dependency"
@@ -160,8 +143,8 @@ def get_sub_dependant(
security_requirement = None
security_scopes = security_scopes or []
if isinstance(depends, params.Security):
- dependency_scopes = depends.scopes
- security_scopes.extend(dependency_scopes)
+ if depends.scopes:
+ security_scopes.extend(depends.scopes)
if isinstance(dependency, SecurityBase):
use_scopes: List[str] = []
if isinstance(dependency, (OAuth2, OpenIdConnect)):
@@ -282,9 +265,6 @@ def get_dependant(
security_scopes: Optional[List[str]] = None,
use_cache: bool = True,
) -> Dependant:
- path_param_names = get_path_param_names(path)
- endpoint_signature = get_typed_signature(call)
- signature_params = endpoint_signature.parameters
dependant = Dependant(
call=call,
name=name,
@@ -292,6 +272,9 @@ def get_dependant(
security_scopes=security_scopes,
use_cache=use_cache,
)
+ path_param_names = get_path_param_names(path)
+ endpoint_signature = get_typed_signature(call)
+ signature_params = endpoint_signature.parameters
for param_name, param in signature_params.items():
is_path_param = param_name in path_param_names
param_details = analyze_param(
@@ -301,10 +284,12 @@ def get_dependant(
is_path_param=is_path_param,
)
if param_details.depends is not None:
- sub_dependant = get_param_sub_dependant(
- param_name=param_name,
+ assert param_details.depends.dependency
+ sub_dependant = get_sub_dependant(
depends=param_details.depends,
+ dependency=param_details.depends.dependency,
path=path,
+ name=param_name,
security_scopes=security_scopes,
)
dependant.dependencies.append(sub_dependant)
diff --git a/fastapi/params.py b/fastapi/params.py
index e85375018..2dc04be14 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -1,4 +1,5 @@
import warnings
+from dataclasses import dataclass
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
@@ -761,26 +762,12 @@ class File(Form): # type: ignore[misc]
)
+@dataclass
class Depends:
- def __init__(
- self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True
- ):
- self.dependency = dependency
- self.use_cache = use_cache
-
- def __repr__(self) -> str:
- attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
- cache = "" if self.use_cache else ", use_cache=False"
- return f"{self.__class__.__name__}({attr}{cache})"
+ dependency: Optional[Callable[..., Any]] = None
+ use_cache: bool = True
+@dataclass
class Security(Depends):
- def __init__(
- self,
- dependency: Optional[Callable[..., Any]] = None,
- *,
- scopes: Optional[Sequence[str]] = None,
- use_cache: bool = True,
- ):
- super().__init__(dependency=dependency, use_cache=use_cache)
- self.scopes = scopes or []
+ scopes: Optional[Sequence[str]] = None
diff --git a/tests/test_no_schema_split.py b/tests/test_no_schema_split.py
new file mode 100644
index 000000000..b0b5958c1
--- /dev/null
+++ b/tests/test_no_schema_split.py
@@ -0,0 +1,203 @@
+# Test with parts from, and to verify the report in:
+# https://github.com/fastapi/fastapi/discussions/14177
+# Made an issue in:
+# https://github.com/fastapi/fastapi/issues/14247
+from enum import Enum
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import BaseModel, Field
+
+from tests.utils import pydantic_snapshot
+
+
+class MessageEventType(str, Enum):
+ alpha = "alpha"
+ beta = "beta"
+
+
+class MessageEvent(BaseModel):
+ event_type: MessageEventType = Field(default=MessageEventType.alpha)
+ output: str
+
+
+class MessageOutput(BaseModel):
+ body: str = ""
+ events: List[MessageEvent] = []
+
+
+class Message(BaseModel):
+ input: str
+ output: MessageOutput
+
+
+app = FastAPI(title="Minimal FastAPI App", version="1.0.0")
+
+
+@app.post("/messages", response_model=Message)
+async def create_message(input_message: str) -> Message:
+ return Message(
+ input=input_message,
+ output=MessageOutput(body=f"Processed: {input_message}"),
+ )
+
+
+client = TestClient(app)
+
+
+def test_create_message():
+ response = client.post("/messages", params={"input_message": "Hello"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "input": "Hello",
+ "output": {"body": "Processed: Hello", "events": []},
+ }
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "Minimal FastAPI App", "version": "1.0.0"},
+ "paths": {
+ "/messages": {
+ "post": {
+ "summary": "Create Message",
+ "operationId": "create_message_messages_post",
+ "parameters": [
+ {
+ "name": "input_message",
+ "in": "query",
+ "required": True,
+ "schema": {"type": "string", "title": "Input Message"},
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Message"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Message": {
+ "properties": {
+ "input": {"type": "string", "title": "Input"},
+ "output": {"$ref": "#/components/schemas/MessageOutput"},
+ },
+ "type": "object",
+ "required": ["input", "output"],
+ "title": "Message",
+ },
+ "MessageEvent": {
+ "properties": {
+ "event_type": pydantic_snapshot(
+ v2=snapshot(
+ {
+ "$ref": "#/components/schemas/MessageEventType",
+ "default": "alpha",
+ }
+ ),
+ v1=snapshot(
+ {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/MessageEventType"
+ }
+ ],
+ "default": "alpha",
+ }
+ ),
+ ),
+ "output": {"type": "string", "title": "Output"},
+ },
+ "type": "object",
+ "required": ["output"],
+ "title": "MessageEvent",
+ },
+ "MessageEventType": pydantic_snapshot(
+ v2=snapshot(
+ {
+ "type": "string",
+ "enum": ["alpha", "beta"],
+ "title": "MessageEventType",
+ }
+ ),
+ v1=snapshot(
+ {
+ "type": "string",
+ "enum": ["alpha", "beta"],
+ "title": "MessageEventType",
+ "description": "An enumeration.",
+ }
+ ),
+ ),
+ "MessageOutput": {
+ "properties": {
+ "body": {"type": "string", "title": "Body", "default": ""},
+ "events": {
+ "items": {"$ref": "#/components/schemas/MessageEvent"},
+ "type": "array",
+ "title": "Events",
+ "default": [],
+ },
+ },
+ "type": "object",
+ "title": "MessageOutput",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
+ )
diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py
index bfc7bed09..baa172497 100644
--- a/tests/test_params_repr.py
+++ b/tests/test_params_repr.py
@@ -1,7 +1,7 @@
from typing import Any, List
from dirty_equals import IsOneOf
-from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query
+from fastapi.params import Body, Cookie, Header, Param, Path, Query
test_data: List[Any] = ["teststr", None, ..., 1, []]
@@ -141,12 +141,3 @@ def test_body_repr_number():
def test_body_repr_list():
assert repr(Body([])) == "Body([])"
-
-
-def test_depends_repr():
- assert repr(Depends()) == "Depends(NoneType)"
- assert repr(Depends(get_user)) == "Depends(get_user)"
- assert repr(Depends(use_cache=False)) == "Depends(NoneType, use_cache=False)"
- assert (
- repr(Depends(get_user, use_cache=False)) == "Depends(get_user, use_cache=False)"
- )
diff --git a/tests/test_pydantic_v1_v2_multifile/test_multifile.py b/tests/test_pydantic_v1_v2_multifile/test_multifile.py
index 4472bd73e..e66d102fb 100644
--- a/tests/test_pydantic_v1_v2_multifile/test_multifile.py
+++ b/tests/test_pydantic_v1_v2_multifile/test_multifile.py
@@ -1028,17 +1028,6 @@ def test_openapi_schema():
"type": "object",
"title": "HTTPValidationError",
},
- "SubItem-Output": {
- "properties": {
- "new_sub_name": {
- "type": "string",
- "title": "New Sub Name",
- }
- },
- "type": "object",
- "required": ["new_sub_name"],
- "title": "SubItem",
- },
"ValidationError": {
"properties": {
"loc": {
@@ -1113,11 +1102,11 @@ def test_openapi_schema():
"title": "New Description",
},
"new_sub": {
- "$ref": "#/components/schemas/SubItem-Output"
+ "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"new_multi": {
"items": {
- "$ref": "#/components/schemas/SubItem-Output"
+ "$ref": "#/components/schemas/tests__test_pydantic_v1_v2_multifile__modelsv2__SubItem"
},
"type": "array",
"title": "New Multi",