Browse Source

Add skip_defaults support for path operations (for #242) (#248)

pull/262/head
William Hayes 6 years ago
committed by Sebastián Ramírez
parent
commit
d8716f94ae
  1. 6
      docs/src/response_model/tutorial001.py
  2. 36
      docs/src/response_model/tutorial004.py
  3. 101
      docs/tutorial/response-model.md
  4. 20
      fastapi/applications.py
  5. 12
      fastapi/encoders.py
  6. 37
      fastapi/routing.py
  7. 177
      tests/test_tutorial/test_response_model/test_tutorial004.py

6
docs/src/response_model/tutorial001.py

@ -1,4 +1,4 @@
from typing import Set
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
@ -11,9 +11,9 @@ class Item(BaseModel):
description: str = None
price: float
tax: float = None
tags: Set[str] = []
tags: List[str] = []
@app.post("/items/", response_model=Item)
async def create_item(*, item: Item):
async def create_item(item: Item):
return item

36
docs/src/response_model/tutorial004.py

@ -0,0 +1,36 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
def read_item(item_id: str):
return items[item_id]
@app.patch("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(skip_defaults=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = updated_item
return updated_item

101
docs/tutorial/response-model.md

@ -82,6 +82,107 @@ And both models will be used for the interactive API documentation:
<img src="/img/tutorial/response-model/image02.png">
## Response Model encoding parameters
If your response model has default values, like:
```Python hl_lines="11 13 14"
{!./src/response_model/tutorial004.py!}
```
* `description: str = None` has a default of `None`.
* `tax: float = None` has a default of `None`.
* `tags: List[str] = []` has a default of an empty list: `[]`.
You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
```Python hl_lines="24"
{!./src/response_model/tutorial004.py!}
```
and those default values won't be included in the response.
So, if you send a request to that *path operation* for the item with ID `foo`, the response (not including default values) will be:
```JSON
{
"name": "Foo",
"price": 50.2
}
```
!!! info
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.
### Data with values for fields with defaults
But if your data has values for the model's fields with default values, like the item with ID `bar`:
```Python hl_lines="3 5"
{
"name": "Bar",
"description": "The bartenders",
"price": 62,
"tax": 20.2
}
```
they will be included in the response.
### Data with the same values as the defaults
If the data has the same values as the default ones, like the item with ID `baz`:
```Python hl_lines="3 5 6"
{
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": []
}
```
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).
So, they will be included in the JSON response.
!!! tip
Notice that the default values can be anything, not only `None`.
They can be a list (`[]`), a `float` of `10.5`, etc.
### Use cases
This is very useful in several scenarios.
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.
### Using Pydantic's `skip_defaults` directly
You can also use your model's `.dict(skip_defaults=True)` in your code.
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:
```Python hl_lines="31 32 33 34 35"
{!./src/response_model/tutorial004.py!}
```
!!! tip
It's common to use the HTTP `PUT` operation to update data.
In theory, `PUT` should be used to "replace" the entire contents.
The less known HTTP `PATCH` operation is also used to update data.
But `PATCH` is expected to be used when *partially* updating data. Instead of *replacing* the entire content.
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.
You can use `PUT` or `PATCH` however you wish.
## Recap
Use the path operation decorator's parameter `response_model` to define response models and especially to ensure private data is filtered out.
Use `response_model_skip_defaults` to return only the values explicitly set.

20
fastapi/applications.py

@ -138,6 +138,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -156,6 +157,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -176,6 +178,7 @@ class FastAPI(Starlette):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -195,6 +198,7 @@ class FastAPI(Starlette):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -246,6 +250,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -262,6 +267,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -281,6 +287,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -297,6 +304,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -316,6 +324,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -332,6 +341,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -351,6 +361,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -367,6 +378,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -386,6 +398,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -402,6 +415,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -421,6 +435,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -437,6 +452,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -456,6 +472,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -472,6 +489,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -491,6 +509,7 @@ class FastAPI(Starlette):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -507,6 +526,7 @@ class FastAPI(Starlette):
responses=responses or {},
deprecated=deprecated,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

12
fastapi/encoders.py

@ -11,6 +11,7 @@ def jsonable_encoder(
include: Set[str] = None,
exclude: Set[str] = set(),
by_alias: bool = True,
skip_defaults: bool = False,
include_none: bool = True,
custom_encoder: dict = {},
sqlalchemy_safe: bool = True,
@ -18,7 +19,12 @@ def jsonable_encoder(
if isinstance(obj, BaseModel):
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
return jsonable_encoder(
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
obj.dict(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
),
include_none=include_none,
custom_encoder=encoder,
sqlalchemy_safe=sqlalchemy_safe,
@ -42,6 +48,7 @@ def jsonable_encoder(
encoded_key = jsonable_encoder(
key,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@ -49,6 +56,7 @@ def jsonable_encoder(
encoded_value = jsonable_encoder(
value,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@ -64,6 +72,7 @@ def jsonable_encoder(
include=include,
exclude=exclude,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,
@ -91,6 +100,7 @@ def jsonable_encoder(
return jsonable_encoder(
data,
by_alias=by_alias,
skip_defaults=skip_defaults,
include_none=include_none,
custom_encoder=custom_encoder,
sqlalchemy_safe=sqlalchemy_safe,

37
fastapi/routing.py

@ -32,8 +32,11 @@ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLA
from starlette.websockets import WebSocket
def serialize_response(*, field: Field = None, response: Response) -> Any:
encoded = jsonable_encoder(response)
def serialize_response(
*, field: Field = None, response: Response, skip_defaults: bool = False
) -> Any:
encoded = jsonable_encoder(response, skip_defaults=skip_defaults)
if field:
errors = []
value, errors_ = field.validate(encoded, {}, loc=("response",))
@ -43,7 +46,7 @@ def serialize_response(*, field: Field = None, response: Response) -> Any:
errors.extend(errors_)
if errors:
raise ValidationError(errors)
return jsonable_encoder(value)
return jsonable_encoder(value, skip_defaults=skip_defaults)
else:
return encoded
@ -54,6 +57,7 @@ def get_app(
status_code: int = 200,
response_class: Type[Response] = JSONResponse,
response_field: Field = None,
skip_defaults: bool = False,
) -> Callable:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@ -93,7 +97,7 @@ def get_app(
raw_response.background = background_tasks
return raw_response
response_data = serialize_response(
field=response_field, response=raw_response
field=response_field, response=raw_response, skip_defaults=skip_defaults
)
return response_class(
content=response_data,
@ -151,6 +155,7 @@ class APIRoute(routing.Route):
name: str = None,
methods: List[str] = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
) -> None:
@ -210,6 +215,7 @@ class APIRoute(routing.Route):
methods = ["GET"]
self.methods = methods
self.operation_id = operation_id
self.response_model_skip_defaults = response_model_skip_defaults
self.include_in_schema = include_in_schema
self.response_class = response_class
@ -230,6 +236,7 @@ class APIRoute(routing.Route):
status_code=self.status_code,
response_class=self.response_class,
response_field=self.response_field,
skip_defaults=self.response_model_skip_defaults,
)
)
@ -251,6 +258,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -269,6 +277,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -290,6 +299,7 @@ class APIRouter(routing.Router):
deprecated: bool = None,
methods: List[str] = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -309,6 +319,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=methods,
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -363,6 +374,7 @@ class APIRouter(routing.Router):
deprecated=route.deprecated,
methods=route.methods,
operation_id=route.operation_id,
response_model_skip_defaults=route.response_model_skip_defaults,
include_in_schema=route.include_in_schema,
response_class=route.response_class,
name=route.name,
@ -398,10 +410,12 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
) -> Callable:
return self.api_route(
path=path,
response_model=response_model,
@ -415,6 +429,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["GET"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -434,6 +449,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -451,6 +467,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PUT"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -470,6 +487,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -487,6 +505,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["POST"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -506,6 +525,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -523,6 +543,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["DELETE"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -542,6 +563,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -559,6 +581,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["OPTIONS"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -578,6 +601,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -595,6 +619,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["HEAD"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -614,6 +639,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -631,6 +657,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["PATCH"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
@ -650,6 +677,7 @@ class APIRouter(routing.Router):
responses: Dict[Union[int, str], Dict[str, Any]] = None,
deprecated: bool = None,
operation_id: str = None,
response_model_skip_defaults: bool = False,
include_in_schema: bool = True,
response_class: Type[Response] = JSONResponse,
name: str = None,
@ -667,6 +695,7 @@ class APIRouter(routing.Router):
deprecated=deprecated,
methods=["TRACE"],
operation_id=operation_id,
response_model_skip_defaults=response_model_skip_defaults,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,

177
tests/test_tutorial/test_response_model/test_tutorial004.py

@ -0,0 +1,177 @@
import pytest
from starlette.testclient import TestClient
from response_model.tutorial004 import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/{item_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Item",
"operationId": "read_item_items__item_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
},
"patch": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Update Item",
"operationId": "update_item_items__item_id__patch",
"parameters": [
{
"required": True,
"schema": {"title": "Item_Id", "type": "string"},
"name": "item_id",
"in": "path",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Item"}
}
},
"required": True,
},
},
}
},
"components": {
"schemas": {
"Item": {
"title": "Item",
"required": ["name", "price"],
"type": "object",
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"description": {"title": "Description", "type": "string"},
"tax": {"title": "Tax", "type": "number", "default": 10.5},
"tags": {
"title": "Tags",
"type": "array",
"items": {"type": "string"},
"default": [],
},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"type": "string"},
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
@pytest.mark.parametrize(
"url,data",
[
("/items/foo", {"name": "Foo", "price": 50.2}),
(
"/items/bar",
{"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
),
(
"/items/baz",
{
"name": "Baz",
"description": None,
"price": 50.2,
"tax": 10.5,
"tags": [],
},
),
],
)
def test_get(url, data):
response = client.get(url)
assert response.status_code == 200
assert response.json() == data
def test_patch():
response = client.patch(
"/items/bar", json={"name": "Barz", "price": 3, "description": None}
)
assert response.json() == {
"name": "Barz",
"description": None,
"price": 3,
"tax": 20.2,
}
Loading…
Cancel
Save