Skip to content

Commit d8716f9

Browse files
wshayestiangolo
authored andcommitted
✨ Add skip_defaults support for path operations (for fastapi#242) (fastapi#248)
1 parent 67f8cb3 commit d8716f9

File tree

7 files changed

+381
-8
lines changed

7 files changed

+381
-8
lines changed

docs/src/response_model/tutorial001.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Set
1+
from typing import List
22

33
from fastapi import FastAPI
44
from pydantic import BaseModel
@@ -11,9 +11,9 @@ class Item(BaseModel):
1111
description: str = None
1212
price: float
1313
tax: float = None
14-
tags: Set[str] = []
14+
tags: List[str] = []
1515

1616

1717
@app.post("/items/", response_model=Item)
18-
async def create_item(*, item: Item):
18+
async def create_item(item: Item):
1919
return item
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import List
2+
3+
from fastapi import FastAPI
4+
from pydantic import BaseModel
5+
6+
app = FastAPI()
7+
8+
9+
class Item(BaseModel):
10+
name: str
11+
description: str = None
12+
price: float
13+
tax: float = 10.5
14+
tags: List[str] = []
15+
16+
17+
items = {
18+
"foo": {"name": "Foo", "price": 50.2},
19+
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
20+
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
21+
}
22+
23+
24+
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
25+
def read_item(item_id: str):
26+
return items[item_id]
27+
28+
29+
@app.patch("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
30+
async def update_item(item_id: str, item: Item):
31+
stored_item_data = items[item_id]
32+
stored_item_model = Item(**stored_item_data)
33+
update_data = item.dict(skip_defaults=True)
34+
updated_item = stored_item_model.copy(update=update_data)
35+
items[item_id] = updated_item
36+
return updated_item

docs/tutorial/response-model.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,107 @@ And both models will be used for the interactive API documentation:
8282

8383
<img src="/img/tutorial/response-model/image02.png">
8484

85+
## Response Model encoding parameters
86+
87+
If your response model has default values, like:
88+
89+
```Python hl_lines="11 13 14"
90+
{!./src/response_model/tutorial004.py!}
91+
```
92+
93+
* `description: str = None` has a default of `None`.
94+
* `tax: float = None` has a default of `None`.
95+
* `tags: List[str] = []` has a default of an empty list: `[]`.
96+
97+
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
98+
99+
```Python hl_lines="24"
100+
{!./src/response_model/tutorial004.py!}
101+
```
102+
103+
and those default values won't be included in the response.
104+
105+
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
106+
107+
```JSON
108+
{
109+
"name": "Foo",
110+
"price": 50.2
111+
}
112+
```
113+
114+
!!! info
115+
FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/#copying" target="_blank">its `skip_defaults` parameter</a> to achieve this.
116+
117+
### Data with values for fields with defaults
118+
119+
But if your data has values for the model's fields with default values, like the item with ID `bar`:
120+
121+
```Python hl_lines="3 5"
122+
{
123+
"name": "Bar",
124+
"description": "The bartenders",
125+
"price": 62,
126+
"tax": 20.2
127+
}
128+
```
129+
130+
they will be included in the response.
131+
132+
### Data with the same values as the defaults
133+
134+
If the data has the same values as the default ones, like the item with ID `baz`:
135+
136+
```Python hl_lines="3 5 6"
137+
{
138+
"name": "Baz",
139+
"description": None,
140+
"price": 50.2,
141+
"tax": 10.5,
142+
"tags": []
143+
}
144+
```
145+
146+
FastAPI is smart enough (actually, Pydantic is smart enough) to realize that, even though `description`, `tax`, and `tags` have the same values as the defaults, they were set explicitly (instead of taken from the defaults).
147+
148+
So, they will be included in the JSON response.
149+
150+
!!! tip
151+
Notice that the default values can be anything, not only `None`.
152+
153+
They can be a list (`[]`), a `float` of `10.5`, etc.
154+
155+
### Use cases
156+
157+
This is very useful in several scenarios.
158+
159+
For example if you have models with many optional attributes in a NoSQL database, but you don't want to send very long JSON responses full of default values.
160+
161+
### Using Pydantic's `skip_defaults` directly
162+
163+
You can also use your model's `.dict(skip_defaults=True)` in your code.
164+
165+
For example, you could receive a model object as a body payload, and update your stored data using only the attributes set, not the default ones:
166+
167+
```Python hl_lines="31 32 33 34 35"
168+
{!./src/response_model/tutorial004.py!}
169+
```
170+
171+
!!! tip
172+
It's common to use the HTTP `PUT` operation to update data.
173+
174+
In theory, `PUT` should be used to "replace" the entire contents.
175+
176+
The less known HTTP `PATCH` operation is also used to update data.
177+
178+
But `PATCH` is expected to be used when *partially* updating data. Instead of *replacing* the entire content.
179+
180+
Still, this is just a small detail, and many teams and code bases use `PUT` instead of `PATCH` for all updates, including to *partially* update contents.
181+
182+
You can use `PUT` or `PATCH` however you wish.
183+
85184
## Recap
86185

87186
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
187+
188+
Use `response_model_skip_defaults` to return only the values explicitly set.

fastapi/applications.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def add_api_route(
138138
deprecated: bool = None,
139139
methods: List[str] = None,
140140
operation_id: str = None,
141+
response_model_skip_defaults: bool = False,
141142
include_in_schema: bool = True,
142143
response_class: Type[Response] = JSONResponse,
143144
name: str = None,
@@ -156,6 +157,7 @@ def add_api_route(
156157
deprecated=deprecated,
157158
methods=methods,
158159
operation_id=operation_id,
160+
response_model_skip_defaults=response_model_skip_defaults,
159161
include_in_schema=include_in_schema,
160162
response_class=response_class,
161163
name=name,
@@ -176,6 +178,7 @@ def api_route(
176178
deprecated: bool = None,
177179
methods: List[str] = None,
178180
operation_id: str = None,
181+
response_model_skip_defaults: bool = False,
179182
include_in_schema: bool = True,
180183
response_class: Type[Response] = JSONResponse,
181184
name: str = None,
@@ -195,6 +198,7 @@ def decorator(func: Callable) -> Callable:
195198
deprecated=deprecated,
196199
methods=methods,
197200
operation_id=operation_id,
201+
response_model_skip_defaults=response_model_skip_defaults,
198202
include_in_schema=include_in_schema,
199203
response_class=response_class,
200204
name=name,
@@ -246,6 +250,7 @@ def get(
246250
responses: Dict[Union[int, str], Dict[str, Any]] = None,
247251
deprecated: bool = None,
248252
operation_id: str = None,
253+
response_model_skip_defaults: bool = False,
249254
include_in_schema: bool = True,
250255
response_class: Type[Response] = JSONResponse,
251256
name: str = None,
@@ -262,6 +267,7 @@ def get(
262267
responses=responses or {},
263268
deprecated=deprecated,
264269
operation_id=operation_id,
270+
response_model_skip_defaults=response_model_skip_defaults,
265271
include_in_schema=include_in_schema,
266272
response_class=response_class,
267273
name=name,
@@ -281,6 +287,7 @@ def put(
281287
responses: Dict[Union[int, str], Dict[str, Any]] = None,
282288
deprecated: bool = None,
283289
operation_id: str = None,
290+
response_model_skip_defaults: bool = False,
284291
include_in_schema: bool = True,
285292
response_class: Type[Response] = JSONResponse,
286293
name: str = None,
@@ -297,6 +304,7 @@ def put(
297304
responses=responses or {},
298305
deprecated=deprecated,
299306
operation_id=operation_id,
307+
response_model_skip_defaults=response_model_skip_defaults,
300308
include_in_schema=include_in_schema,
301309
response_class=response_class,
302310
name=name,
@@ -316,6 +324,7 @@ def post(
316324
responses: Dict[Union[int, str], Dict[str, Any]] = None,
317325
deprecated: bool = None,
318326
operation_id: str = None,
327+
response_model_skip_defaults: bool = False,
319328
include_in_schema: bool = True,
320329
response_class: Type[Response] = JSONResponse,
321330
name: str = None,
@@ -332,6 +341,7 @@ def post(
332341
responses=responses or {},
333342
deprecated=deprecated,
334343
operation_id=operation_id,
344+
response_model_skip_defaults=response_model_skip_defaults,
335345
include_in_schema=include_in_schema,
336346
response_class=response_class,
337347
name=name,
@@ -351,6 +361,7 @@ def delete(
351361
responses: Dict[Union[int, str], Dict[str, Any]] = None,
352362
deprecated: bool = None,
353363
operation_id: str = None,
364+
response_model_skip_defaults: bool = False,
354365
include_in_schema: bool = True,
355366
response_class: Type[Response] = JSONResponse,
356367
name: str = None,
@@ -367,6 +378,7 @@ def delete(
367378
responses=responses or {},
368379
deprecated=deprecated,
369380
operation_id=operation_id,
381+
response_model_skip_defaults=response_model_skip_defaults,
370382
include_in_schema=include_in_schema,
371383
response_class=response_class,
372384
name=name,
@@ -386,6 +398,7 @@ def options(
386398
responses: Dict[Union[int, str], Dict[str, Any]] = None,
387399
deprecated: bool = None,
388400
operation_id: str = None,
401+
response_model_skip_defaults: bool = False,
389402
include_in_schema: bool = True,
390403
response_class: Type[Response] = JSONResponse,
391404
name: str = None,
@@ -402,6 +415,7 @@ def options(
402415
responses=responses or {},
403416
deprecated=deprecated,
404417
operation_id=operation_id,
418+
response_model_skip_defaults=response_model_skip_defaults,
405419
include_in_schema=include_in_schema,
406420
response_class=response_class,
407421
name=name,
@@ -421,6 +435,7 @@ def head(
421435
responses: Dict[Union[int, str], Dict[str, Any]] = None,
422436
deprecated: bool = None,
423437
operation_id: str = None,
438+
response_model_skip_defaults: bool = False,
424439
include_in_schema: bool = True,
425440
response_class: Type[Response] = JSONResponse,
426441
name: str = None,
@@ -437,6 +452,7 @@ def head(
437452
responses=responses or {},
438453
deprecated=deprecated,
439454
operation_id=operation_id,
455+
response_model_skip_defaults=response_model_skip_defaults,
440456
include_in_schema=include_in_schema,
441457
response_class=response_class,
442458
name=name,
@@ -456,6 +472,7 @@ def patch(
456472
responses: Dict[Union[int, str], Dict[str, Any]] = None,
457473
deprecated: bool = None,
458474
operation_id: str = None,
475+
response_model_skip_defaults: bool = False,
459476
include_in_schema: bool = True,
460477
response_class: Type[Response] = JSONResponse,
461478
name: str = None,
@@ -472,6 +489,7 @@ def patch(
472489
responses=responses or {},
473490
deprecated=deprecated,
474491
operation_id=operation_id,
492+
response_model_skip_defaults=response_model_skip_defaults,
475493
include_in_schema=include_in_schema,
476494
response_class=response_class,
477495
name=name,
@@ -491,6 +509,7 @@ def trace(
491509
responses: Dict[Union[int, str], Dict[str, Any]] = None,
492510
deprecated: bool = None,
493511
operation_id: str = None,
512+
response_model_skip_defaults: bool = False,
494513
include_in_schema: bool = True,
495514
response_class: Type[Response] = JSONResponse,
496515
name: str = None,
@@ -507,6 +526,7 @@ def trace(
507526
responses=responses or {},
508527
deprecated=deprecated,
509528
operation_id=operation_id,
529+
response_model_skip_defaults=response_model_skip_defaults,
510530
include_in_schema=include_in_schema,
511531
response_class=response_class,
512532
name=name,

fastapi/encoders.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ def jsonable_encoder(
1111
include: Set[str] = None,
1212
exclude: Set[str] = set(),
1313
by_alias: bool = True,
14+
skip_defaults: bool = False,
1415
include_none: bool = True,
1516
custom_encoder: dict = {},
1617
sqlalchemy_safe: bool = True,
1718
) -> Any:
1819
if isinstance(obj, BaseModel):
1920
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
2021
return jsonable_encoder(
21-
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
22+
obj.dict(
23+
include=include,
24+
exclude=exclude,
25+
by_alias=by_alias,
26+
skip_defaults=skip_defaults,
27+
),
2228
include_none=include_none,
2329
custom_encoder=encoder,
2430
sqlalchemy_safe=sqlalchemy_safe,
@@ -42,13 +48,15 @@ def jsonable_encoder(
4248
encoded_key = jsonable_encoder(
4349
key,
4450
by_alias=by_alias,
51+
skip_defaults=skip_defaults,
4552
include_none=include_none,
4653
custom_encoder=custom_encoder,
4754
sqlalchemy_safe=sqlalchemy_safe,
4855
)
4956
encoded_value = jsonable_encoder(
5057
value,
5158
by_alias=by_alias,
59+
skip_defaults=skip_defaults,
5260
include_none=include_none,
5361
custom_encoder=custom_encoder,
5462
sqlalchemy_safe=sqlalchemy_safe,
@@ -64,6 +72,7 @@ def jsonable_encoder(
6472
include=include,
6573
exclude=exclude,
6674
by_alias=by_alias,
75+
skip_defaults=skip_defaults,
6776
include_none=include_none,
6877
custom_encoder=custom_encoder,
6978
sqlalchemy_safe=sqlalchemy_safe,
@@ -91,6 +100,7 @@ def jsonable_encoder(
91100
return jsonable_encoder(
92101
data,
93102
by_alias=by_alias,
103+
skip_defaults=skip_defaults,
94104
include_none=include_none,
95105
custom_encoder=custom_encoder,
96106
sqlalchemy_safe=sqlalchemy_safe,

0 commit comments

Comments
 (0)