diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3940be01..1aa02ce26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: - "3.10" - "3.9" - "3.8" - pydantic-version: ["pydantic-v1", "pydantic-v2"] + pydantic-version: ["pydantic-v1", "pydantic-v2.7", "pydantic-v2.8+"] fail-fast: false steps: - name: Dump GitHub context @@ -80,8 +80,11 @@ jobs: if: matrix.pydantic-version == 'pydantic-v1' run: uv pip install "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 - if: matrix.pydantic-version == 'pydantic-v2' - run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" + if: matrix.pydantic-version == 'pydantic-v2.7' + run: uv pip install --upgrade "pydantic>=2.0.2,<2.8" + - name: Install Pydantic v2.8+ + if: matrix.pydantic-version == 'pydantic-v2.8+' + run: uv pip install --upgrade "pydantic>=2.8,<3.0" # TODO: Remove this once Python 3.8 is no longer supported - name: Install older AnyIO in Python 3.8 if: matrix.python-version == '3.8' diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 227ad837d..88487f3e5 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -11,6 +11,7 @@ from typing import ( FrozenSet, List, Mapping, + Optional, Sequence, Set, Tuple, @@ -69,8 +70,8 @@ if PYDANTIC_V2: with_info_plain_validator_function as with_info_plain_validator_function, ) except ImportError: # pragma: no cover - from pydantic_core.core_schema import ( - general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + from pydantic_core.core_schema import ( # noqa: F401 + general_plain_validator_function as with_info_plain_validator_function, ) RequiredParam = PydanticUndefined @@ -146,19 +147,35 @@ if PYDANTIC_V2: exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + context: Optional[Dict[str, Any]] = None, ) -> Any: # What calls this code passes a value that already called # self._type_adapter.validate_python(value) - return self._type_adapter.dump_python( - value, - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + # + # context argument was introduced in pydantic 2.8 + if PYDANTIC_VERSION >= "2.8": + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + context=context, + ) + else: + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) def __hash__(self) -> int: # Each ModelField is unique for our purposes, to allow making a dict from diff --git a/fastapi/applications.py b/fastapi/applications.py index 05c7bd2be..89403150c 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,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1822,6 +1841,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 +2104,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2200,6 +2235,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 +2498,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2578,6 +2629,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 +2892,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2951,6 +3018,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 +3281,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3324,6 +3407,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 +3670,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3697,6 +3796,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 +4059,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4075,6 +4190,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 +4453,21 @@ class FastAPI(Starlette): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4448,6 +4579,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 54c75a027..7717dd834 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -152,6 +152,7 @@ async def serialize_response( exclude_defaults: bool = False, exclude_none: bool = False, is_coroutine: bool = True, + context: Optional[Dict[str, Any]] = None, ) -> Any: if field: errors = [] @@ -187,6 +188,7 @@ async def serialize_response( exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, + context=context, ) return jsonable_encoder( @@ -227,6 +229,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, embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: @@ -335,6 +338,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): @@ -450,6 +454,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 @@ -480,6 +485,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 @@ -582,6 +588,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, embed_body_fields=self._embed_body_fields, ) @@ -901,6 +908,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 @@ -951,6 +959,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, @@ -982,6 +991,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, @@ -1012,6 +1022,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, @@ -1320,6 +1331,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, @@ -1618,6 +1630,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -1733,6 +1760,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, @@ -1995,6 +2023,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2115,6 +2158,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, @@ -2377,6 +2421,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2497,6 +2556,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, @@ -2759,6 +2819,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -2874,6 +2949,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, @@ -3136,6 +3212,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3251,6 +3342,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, @@ -3513,6 +3605,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -3633,6 +3740,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, @@ -3895,6 +4003,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4015,6 +4138,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, @@ -4277,6 +4401,21 @@ class APIRouter(routing.Router): """ ), ] = False, + response_model_context: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Additional context to pass to Pydantic when creating the response. + + This will be passed in as serialization context to the response model. + + Note: This feature is a noop on pydantic < 2.8 + + Read more about serialization context in the + [Pydantic documentation](https://docs.pydantic.dev/latest/concepts/serialization/#serialization-context) + """ + ), + ] = None, include_in_schema: Annotated[ bool, Doc( @@ -4397,6 +4536,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..8ac3a5f1f 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,6 +1,8 @@ from typing import Dict, List, Optional +import pytest from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2, PYDANTIC_VERSION from pydantic import BaseModel, Field from starlette.testclient import TestClient @@ -152,3 +154,67 @@ def test_validdict_exclude_unset(): "k2": {"aliased_name": "bar", "price": 1.0}, "k3": {"aliased_name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, } + + +if PYDANTIC_V2: + from pydantic import SerializationInfo, model_serializer + + class MultiUseItem(BaseModel): + name: str = Field(alias="aliased_name") + secret: Optional[str] = None + owner_ids: Optional[List[int]] = None + + @model_serializer(mode="wrap") + def _serialize(self, handler, info: SerializationInfo): + data = handler(self) + if info.context and info.context.get("mode") == "FASTAPI": + if "secret" in data: + data.pop("secret") + return data + + app_v2 = FastAPI() + + @app_v2.get( + "/items/validdict-with-context", + response_model=Dict[str, MultiUseItem], + response_model_context={"mode": "FASTAPI"}, + ) + async def get_validdict_with_context(): + return { + "k1": MultiUseItem(aliased_name="foo"), + "k2": MultiUseItem(aliased_name="bar", secret="sEcReT"), + "k3": MultiUseItem( + aliased_name="baz", secret="sEcReT", owner_ids=[1, 2, 3] + ), + } + + client_v2 = TestClient(app_v2) + + @pytest.mark.skipif(PYDANTIC_VERSION < "2.8", reason="requires Pydantic v2.8+") + def test_validdict_with_context__pydantic_supported(): + response = client_v2.get("/items/validdict-with-context") + response.raise_for_status() + + expected_response = { + "k1": {"aliased_name": "foo", "owner_ids": None}, + "k2": {"aliased_name": "bar", "owner_ids": None}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3]}, + } + + assert response.json() == expected_response + + @pytest.mark.skipif( + PYDANTIC_VERSION >= "2.8", + reason="Pydantic supports the feature from this point on", + ) + def test_validdict_with_context__pre_pydantic_support(): + response = client_v2.get("/items/validdict-with-context") + response.raise_for_status() + + expected_response = { + "k1": {"aliased_name": "foo", "owner_ids": None, "secret": None}, + "k2": {"aliased_name": "bar", "owner_ids": None, "secret": "sEcReT"}, + "k3": {"aliased_name": "baz", "owner_ids": [1, 2, 3], "secret": "sEcReT"}, + } + + assert response.json() == expected_response