From 3d7e369eae029de8e3bd81a4210efd23ae993271 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Thu, 23 May 2024 23:30:29 +0000 Subject: [PATCH] Allow serialization with context --- fastapi/_compat.py | 2 ++ fastapi/applications.py | 36 +++++++++++++++++++++ fastapi/routing.py | 44 ++++++++++++++++++++++++++ tests/test_serialize_response_model.py | 32 +++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 06b847b4f..714c9c4d2 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -143,6 +143,7 @@ if PYDANTIC_V2: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + context: dict[str, Any] | None = None, ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) @@ -155,6 +156,7 @@ if PYDANTIC_V2: exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, + context=context, ) def __hash__(self) -> int: diff --git a/fastapi/applications.py b/fastapi/applications.py index 4f5e6f1d9..acd60899b 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1075,6 +1075,7 @@ class FastAPI(Starlette): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -1105,6 +1106,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1133,6 +1135,7 @@ class FastAPI(Starlette): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, @@ -1162,6 +1165,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1711,6 +1715,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1822,6 +1829,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2084,6 +2092,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2200,6 +2211,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2462,6 +2474,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2578,6 +2593,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2840,6 +2856,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2951,6 +2970,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3213,6 +3233,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3324,6 +3347,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3586,6 +3610,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3697,6 +3724,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3959,6 +3987,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4075,6 +4106,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -4337,6 +4369,9 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4448,6 +4483,7 @@ class FastAPI(Starlette): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, diff --git a/fastapi/routing.py b/fastapi/routing.py index fa1351859..afbcd04ee 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -130,6 +130,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, + context: dict[str, Any] | None = None, ) -> Any: if field: errors = [] @@ -165,6 +166,7 @@ async def serialize_response( exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, + context=context, ) return jsonable_encoder( @@ -205,6 +207,7 @@ def get_request_handler( response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, dependency_overrides_provider: Optional[Any] = None, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" @@ -303,6 +306,7 @@ def get_request_handler( exclude_defaults=response_model_exclude_defaults, exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, + context=response_model_context, ) response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): @@ -410,6 +414,7 @@ class APIRoute(routing.Route): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -440,6 +445,7 @@ class APIRoute(routing.Route): self.response_model_exclude_unset = response_model_exclude_unset self.response_model_exclude_defaults = response_model_exclude_defaults self.response_model_exclude_none = response_model_exclude_none + self.response_model_context = response_model_context self.include_in_schema = include_in_schema self.response_class = response_class self.dependency_overrides_provider = dependency_overrides_provider @@ -532,6 +538,7 @@ class APIRoute(routing.Route): response_model_exclude_unset=self.response_model_exclude_unset, response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_none=self.response_model_exclude_none, + response_model_context=self.response_model_context, dependency_overrides_provider=self.dependency_overrides_provider, ) @@ -850,6 +857,7 @@ class APIRouter(routing.Router): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Union[Type[Response], DefaultPlaceholder] = Default( JSONResponse @@ -900,6 +908,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema and self.include_in_schema, response_class=current_response_class, name=name, @@ -931,6 +940,7 @@ class APIRouter(routing.Router): response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, + response_model_context: Optional[Dict[str, Any]] = None, include_in_schema: bool = True, response_class: Type[Response] = Default(JSONResponse), name: Optional[str] = None, @@ -961,6 +971,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1269,6 +1280,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=route.response_model_exclude_unset, response_model_exclude_defaults=route.response_model_exclude_defaults, response_model_exclude_none=route.response_model_exclude_none, + response_model_context=route.response_model_context, include_in_schema=route.include_in_schema and self.include_in_schema and include_in_schema, @@ -1563,6 +1575,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1678,6 +1693,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -1940,6 +1956,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2060,6 +2079,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2322,6 +2342,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2442,6 +2465,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -2704,6 +2728,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2819,6 +2846,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3081,6 +3109,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3196,6 +3227,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3458,6 +3490,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3578,6 +3613,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -3840,6 +3876,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3960,6 +3999,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, @@ -4222,6 +4262,9 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[dict], Doc("Context to be used when encoding the response model.") + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4342,6 +4385,7 @@ class APIRouter(routing.Router): response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, + response_model_context=response_model_context, include_in_schema=include_in_schema, response_class=response_class, name=name, diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 3bb46b2e9..612d41fc1 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -12,6 +12,14 @@ class Item(BaseModel): price: Optional[float] = None owner_ids: Optional[List[int]] = None + @model_serializer(mode="wrap") + def _serialize(self, handler, info: SerializationInfo | None = None): + data = handler(self) + if info.context and info.context.get("mode") == "FASTAPI": + if "price" in data: + data.pop("price") + return data + @app.get("/items/valid", response_model=Item) def get_valid(): @@ -83,6 +91,20 @@ def get_validdict_exclude_unset(): } +@app.get( + "/items/validdict-with-context", + response_model=Dict[str, Item], + response_model_context={"mode": "FASTAPI"}, +) +async def get_validdict_with_context(): + + return { + "k1": Item(aliased_name="foo"), + "k2": Item(aliased_name="bar", price=1.0), + "k3": Item(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]), + } + + client = TestClient(app) @@ -152,3 +174,13 @@ def test_validdict_exclude_unset(): "k2": {"aliased_name": "bar", "price": 1.0}, "k3": {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, } + + +def test_validdict_with_context(): + response = client.get("/items/validdict-with-context") + response.raise_for_status() + assert response.json() == { + "k1": {"aliased_name": "foo", "owner_ids": None}, + "k2": {"aliased_name": "bar", "owner_ids": None}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3]}, + }