From 3397d4d69a9c2d64c1219fcbf291ea5697a4abb8 Mon Sep 17 00:00:00 2001 From: voegtlel <5764745+voegtlel@users.noreply.github.com> Date: Sun, 5 Apr 2020 15:04:46 +0200 Subject: [PATCH] :sparkles: Implement response_model_exclude_defaults and response_model_exclude_none (#1166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented response_model_exclude_defaults and response_model_exclude_none to be compatible pydantic options. * :truck: Rename and invert include_none to exclude_none to keep in sync with Pydantic Co-authored-by: Lukas Voegtle Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 40 +++++++++++++++ fastapi/encoders.py | 22 +++++--- fastapi/openapi/utils.py | 4 +- fastapi/routing.py | 93 +++++++++++++++++++++++++++++++--- tests/test_jsonable_encoder.py | 16 ++++++ tests/test_skip_defaults.py | 64 ++++++++++++++++++++++- 6 files changed, 223 insertions(+), 16 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 8270e54fd..84a1b6de2 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -171,6 +171,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -197,6 +199,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -222,6 +226,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -250,6 +256,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -309,6 +317,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -334,6 +344,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -359,6 +371,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -384,6 +398,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -409,6 +425,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -434,6 +452,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -459,6 +479,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -484,6 +506,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -509,6 +533,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -534,6 +560,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -559,6 +587,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -584,6 +614,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -609,6 +641,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -634,6 +668,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -659,6 +695,8 @@ class FastAPI(Starlette): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -684,6 +722,8 @@ class FastAPI(Starlette): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, diff --git a/fastapi/encoders.py b/fastapi/encoders.py index ae4794bae..26ceb2144 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -34,7 +34,8 @@ def jsonable_encoder( by_alias: bool = True, skip_defaults: bool = None, exclude_unset: bool = False, - include_none: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, custom_encoder: dict = {}, sqlalchemy_safe: bool = True, ) -> Any: @@ -58,8 +59,12 @@ def jsonable_encoder( exclude=exclude, by_alias=by_alias, exclude_unset=bool(exclude_unset or skip_defaults), + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, ) else: # pragma: nocover + if exclude_defaults: + raise ValueError("Cannot use exclude_defaults") obj_dict = obj.dict( include=include, exclude=exclude, @@ -68,7 +73,8 @@ def jsonable_encoder( ) return jsonable_encoder( obj_dict, - include_none=include_none, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, custom_encoder=encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -87,14 +93,14 @@ def jsonable_encoder( or (not isinstance(key, str)) or (not key.startswith("_sa")) ) - and (value is not None or include_none) + and (value is not None or not exclude_none) and ((include and key in include) or key not in exclude) ): encoded_key = jsonable_encoder( key, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -102,7 +108,7 @@ def jsonable_encoder( value, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -118,7 +124,8 @@ def jsonable_encoder( exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) @@ -153,7 +160,8 @@ def jsonable_encoder( data, by_alias=by_alias, exclude_unset=exclude_unset, - include_none=include_none, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 91f90ec58..c1e66fc8d 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -81,7 +81,7 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L security_definition = jsonable_encoder( security_requirement.security_scheme.model, by_alias=True, - include_none=False, + exclude_none=True, ) security_name = security_requirement.security_scheme.scheme_name security_definitions[security_name] = security_definition @@ -310,4 +310,4 @@ def get_openapi( if components: output["components"] = components output["paths"] = paths - return jsonable_encoder(OpenAPI(**output), by_alias=True, include_none=False) + return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) diff --git a/fastapi/routing.py b/fastapi/routing.py index 1ec0b693c..3ac420e6e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -49,22 +49,43 @@ except ImportError: # pragma: nocover def _prepare_response_content( - res: Any, *, by_alias: bool = True, exclude_unset: bool + res: Any, + *, + by_alias: bool = True, + exclude_unset: bool, + exclude_defaults: bool = False, + exclude_none: bool = False, ) -> Any: if isinstance(res, BaseModel): if PYDANTIC_1: - return res.dict(by_alias=by_alias, exclude_unset=exclude_unset) + return res.dict( + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) else: return res.dict( - by_alias=by_alias, skip_defaults=exclude_unset + by_alias=by_alias, skip_defaults=exclude_unset, ) # pragma: nocover elif isinstance(res, list): return [ - _prepare_response_content(item, exclude_unset=exclude_unset) for item in res + _prepare_response_content( + item, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + for item in res ] elif isinstance(res, dict): return { - k: _prepare_response_content(v, exclude_unset=exclude_unset) + k: _prepare_response_content( + v, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) for k, v in res.items() } return res @@ -78,12 +99,18 @@ async def serialize_response( exclude: Union[SetIntStr, DictIntStrAny] = set(), by_alias: bool = True, exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, is_coroutine: bool = True, ) -> Any: if field: errors = [] response_content = _prepare_response_content( - response_content, by_alias=by_alias, exclude_unset=exclude_unset + response_content, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) @@ -103,6 +130,8 @@ async def serialize_response( exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, ) else: return jsonable_encoder(response_content) @@ -131,6 +160,8 @@ def get_request_handler( response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, dependency_overrides_provider: Any = None, ) -> Callable: assert dependant.call is not None, "dependant.call must be a function" @@ -177,6 +208,8 @@ def get_request_handler( exclude=response_model_exclude, by_alias=response_model_by_alias, exclude_unset=response_model_exclude_unset, + exclude_defaults=response_model_exclude_defaults, + exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, ) response = response_class( @@ -255,6 +288,8 @@ class APIRoute(routing.Route): response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(), response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Optional[Type[Response]] = None, dependency_overrides_provider: Any = None, @@ -326,6 +361,8 @@ class APIRoute(routing.Route): self.response_model_exclude = response_model_exclude self.response_model_by_alias = response_model_by_alias 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.include_in_schema = include_in_schema self.response_class = response_class @@ -352,6 +389,8 @@ class APIRoute(routing.Route): response_model_exclude=self.response_model_exclude, response_model_by_alias=self.response_model_by_alias, 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, dependency_overrides_provider=self.dependency_overrides_provider, ) @@ -400,6 +439,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -429,6 +470,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -457,6 +500,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -486,6 +531,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -560,6 +607,8 @@ class APIRouter(routing.Router): response_model_exclude=route.response_model_exclude, response_model_by_alias=route.response_model_by_alias, 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, include_in_schema=route.include_in_schema, response_class=route.response_class or default_response_class, name=route.name, @@ -606,6 +655,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -632,6 +683,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -657,6 +710,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -683,6 +738,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -708,6 +765,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -734,6 +793,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -759,6 +820,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -785,6 +848,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -810,6 +875,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -836,6 +903,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -861,6 +930,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -887,6 +958,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -912,6 +985,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -938,6 +1013,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, @@ -963,6 +1040,8 @@ class APIRouter(routing.Router): response_model_by_alias: bool = True, response_model_skip_defaults: bool = None, response_model_exclude_unset: bool = False, + response_model_exclude_defaults: bool = False, + response_model_exclude_none: bool = False, include_in_schema: bool = True, response_class: Type[Response] = None, name: str = None, @@ -989,6 +1068,8 @@ class APIRouter(routing.Router): response_model_exclude_unset=bool( response_model_exclude_unset or response_model_skip_defaults ), + response_model_exclude_defaults=response_model_exclude_defaults, + response_model_exclude_none=response_model_exclude_none, include_in_schema=include_in_schema, response_class=response_class or self.default_response_class, name=name, diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index f94771aae..adee443a8 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -70,6 +70,12 @@ class ModelWithAlias(BaseModel): foo: str = Field(..., alias="Foo") +class ModelWithDefault(BaseModel): + foo: str = ... + bar: str = "bar" + bla: str = "bla" + + @pytest.fixture( name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] ) @@ -121,6 +127,16 @@ def test_encode_model_with_alias(): assert jsonable_encoder(model) == {"Foo": "Bar"} +def test_encode_model_with_default(): + model = ModelWithDefault(foo="foo", bar="bar") + assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} + assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} + assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} + assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { + "foo": "foo" + } + + def test_custom_encoders(): class safe_datetime(datetime): pass diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index c9a85a305..ed84df66c 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -18,11 +18,53 @@ class Model(BaseModel): class ModelSubclass(Model): y: int + z: int = 0 + w: int = None + + +class ModelDefaults(BaseModel): + w: Optional[str] = None + x: Optional[str] = None + y: str = "y" + z: str = "z" @app.get("/", response_model=Model, response_model_exclude_unset=True) def get() -> ModelSubclass: - return ModelSubclass(sub={}, y=1) + return ModelSubclass(sub={}, y=1, z=0) + + +@app.get( + "/exclude_unset", response_model=ModelDefaults, response_model_exclude_unset=True +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_defaults", + response_model=ModelDefaults, + response_model_exclude_defaults=True, +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_none", response_model=ModelDefaults, response_model_exclude_none=True +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") + + +@app.get( + "/exclude_unset_none", + response_model=ModelDefaults, + response_model_exclude_unset=True, + response_model_exclude_none=True, +) +def get() -> ModelDefaults: + return ModelDefaults(x=None, y="y") client = TestClient(app) @@ -31,3 +73,23 @@ client = TestClient(app) def test_return_defaults(): response = client.get("/") assert response.json() == {"sub": {}} + + +def test_return_exclude_unset(): + response = client.get("/exclude_unset") + assert response.json() == {"x": None, "y": "y"} + + +def test_return_exclude_defaults(): + response = client.get("/exclude_defaults") + assert response.json() == {} + + +def test_return_exclude_none(): + response = client.get("/exclude_none") + assert response.json() == {"y": "y", "z": "z"} + + +def test_return_exclude_unset_none(): + response = client.get("/exclude_unset_none") + assert response.json() == {"y": "y"}