]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add include, exclude, and by_alias to path operation methods (#264)
authorSebastián Ramírez <tiangolo@gmail.com>
Mon, 27 May 2019 12:08:13 +0000 (16:08 +0400)
committerGitHub <noreply@github.com>
Mon, 27 May 2019 12:08:13 +0000 (16:08 +0400)
* :sparkles: Make jsonable_encoder's include and exclude receive sequences

* :sparkles: Add include, exclude, and by_alias to app and router

* :sparkles: Add and update tutorial code with new parameters

* :memo: Update docs for new parameters and add docs for updating data

* :white_check_mark: Add tests for consistency in path operation methods

* :white_check_mark: Add tests for new parameters and update tests

17 files changed:
docs/src/body_updates/tutorial001.py [new file with mode: 0644]
docs/src/body_updates/tutorial002.py [new file with mode: 0644]
docs/src/response_model/tutorial004.py
docs/src/response_model/tutorial005.py [new file with mode: 0644]
docs/src/response_model/tutorial006.py [new file with mode: 0644]
docs/tutorial/body-updates.md [new file with mode: 0644]
docs/tutorial/response-model.md
fastapi/applications.py
fastapi/encoders.py
fastapi/routing.py
mkdocs.yml
tests/test_operations_signatures.py [new file with mode: 0644]
tests/test_tutorial/test_body_updates/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_body_updates/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_response_model/test_tutorial004.py
tests/test_tutorial/test_response_model/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_response_model/test_tutorial006.py [new file with mode: 0644]

diff --git a/docs/src/body_updates/tutorial001.py b/docs/src/body_updates/tutorial001.py
new file mode 100644 (file)
index 0000000..bf92493
--- /dev/null
@@ -0,0 +1,34 @@
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.encoders import jsonable_encoder
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str = None
+    description: str = None
+    price: float = None
+    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)
+async def read_item(item_id: str):
+    return items[item_id]
+
+
+@app.put("/items/{item_id}", response_model=Item)
+async def update_item(item_id: str, item: Item):
+    update_item_encoded = jsonable_encoder(item)
+    items[item_id] = update_item_encoded
+    return update_item_encoded
diff --git a/docs/src/body_updates/tutorial002.py b/docs/src/body_updates/tutorial002.py
new file mode 100644 (file)
index 0000000..e10fbb9
--- /dev/null
@@ -0,0 +1,37 @@
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.encoders import jsonable_encoder
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str = None
+    description: str = None
+    price: float = None
+    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)
+async def read_item(item_id: str):
+    return items[item_id]
+
+
+@app.patch("/items/{item_id}", response_model=Item)
+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] = jsonable_encoder(updated_item)
+    return updated_item
index 30ad2184b376dc0ee14b2a323c454336660ce446..bbfd855bbd7eaf1a491c33f3afca824d921ba5b5 100644 (file)
@@ -22,15 +22,5 @@ items = {
 
 
 @app.get("/items/{item_id}", response_model=Item, response_model_skip_defaults=True)
-def read_item(item_id: str):
+async 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
diff --git a/docs/src/response_model/tutorial005.py b/docs/src/response_model/tutorial005.py
new file mode 100644 (file)
index 0000000..65343f0
--- /dev/null
@@ -0,0 +1,37 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str = None
+    price: float
+    tax: float = 10.5
+
+
+items = {
+    "foo": {"name": "Foo", "price": 50.2},
+    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
+    "baz": {
+        "name": "Baz",
+        "description": "There goes my baz",
+        "price": 50.2,
+        "tax": 10.5,
+    },
+}
+
+
+@app.get(
+    "/items/{item_id}/name",
+    response_model=Item,
+    response_model_include={"name", "description"},
+)
+async def read_item_name(item_id: str):
+    return items[item_id]
+
+
+@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
+async def read_item_public_data(item_id: str):
+    return items[item_id]
diff --git a/docs/src/response_model/tutorial006.py b/docs/src/response_model/tutorial006.py
new file mode 100644 (file)
index 0000000..127c6f7
--- /dev/null
@@ -0,0 +1,37 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str = None
+    price: float
+    tax: float = 10.5
+
+
+items = {
+    "foo": {"name": "Foo", "price": 50.2},
+    "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
+    "baz": {
+        "name": "Baz",
+        "description": "There goes my baz",
+        "price": 50.2,
+        "tax": 10.5,
+    },
+}
+
+
+@app.get(
+    "/items/{item_id}/name",
+    response_model=Item,
+    response_model_include=["name", "description"],
+)
+async def read_item_name(item_id: str):
+    return items[item_id]
+
+
+@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"])
+async def read_item_public_data(item_id: str):
+    return items[item_id]
diff --git a/docs/tutorial/body-updates.md b/docs/tutorial/body-updates.md
new file mode 100644 (file)
index 0000000..0850d5d
--- /dev/null
@@ -0,0 +1,97 @@
+## Update replacing with `PUT`
+
+To update an item you can use the [HTTP `PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) operation.
+
+You can use the `jsonable_encoder` to convert the input data to data that can be stored as JSON (e.g. with a NoSQL database). For example, converting `datetime` to `str`.
+
+```Python hl_lines="30 31 32 33 34 35"
+{!./src/body_updates/tutorial001.py!}
+```
+
+`PUT` is used to receive data that should replace the existing data.
+
+### Warning about replacing
+
+That means that if you want to update the item `bar` using `PUT` with a body containing:
+
+```Python
+{
+    "name": "Barz",
+    "price": 3,
+    "description": None,
+}
+```
+
+because it doesn't include the already stored attribute `"tax": 20.2`, the input model would take the default value of `"tax": 10.5`.
+
+And the data would be saved with that "new" `tax` of `10.5`.
+
+## Partial updates with `PATCH`
+
+You can also use the [HTTP `PATCH`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) operation to *partially* update data.
+
+This means that you can send only the data that you want to update, leaving the rest intact.
+
+!!! Note
+    `PATCH` is less commonly used and known than `PUT`.
+
+    And many teams use only `PUT`, even for partial updates.
+
+    You are **free** to use them however you want, **FastAPI** doesn't impose any restrictions.
+
+    But this guide shows you, more or less, how they are intended to be used.
+
+### Using Pydantic's `skip_defaults` parameter
+
+If you want to receive partial updates, it's very useful to use the parameter `skip_defaults` in Pydantic's model's `.dict()`.
+
+Like `item.dict(skip_defaults=True)`.
+
+That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values.
+
+Then you can use this to generate a `dict` with only the data that was set, omitting default values:
+
+```Python hl_lines="34"
+{!./src/body_updates/tutorial002.py!}
+```
+
+### Using Pydantic's `update` parameter
+
+Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update.
+
+Like `stored_item_model.copy(update=update_data)`:
+
+```Python hl_lines="35"
+{!./src/body_updates/tutorial002.py!}
+```
+
+### Partial updates recap
+
+In summary, to apply partial updates you would:
+
+* (Optionally) use `PATCH` instead of `PUT`.
+* Retrieve the stored data.
+* Put that data in a Pydantic model.
+* Generate a `dict` without default values from the input model (using `skip_defaults`).
+    * This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model.
+* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter).
+* Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`).
+    * This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`.
+* Save the data to your DB.
+* Return the updated model.
+
+```Python hl_lines="30 31 32 33 34 35 36 37"
+{!./src/body_updates/tutorial002.py!}
+```
+
+!!! tip
+    You can actually use this same technique with an HTTP `PUT` operation.
+
+    But the example here uses `PATCH` because it was created for these use cases.
+
+!!! note
+    Notice that the input model is still validated.
+
+    So, if you want to receive partial updates that can omit all the attributes, you need to have a model with all the attributes marked as optional (with default values or `None`).
+
+    To distinguish from the models with all optional values for **updates** and models with required values for **creation**, you can use the ideas described in <a href="https://fastapi.tiangolo.com/tutorial/extra-models/" target="_blank">Extra Models</a>.
index 1402a6a8f0860d594def36b304ba4ba17f856314..8b389f2360bcd52885e58ed55a19cdb7d4be3293 100644 (file)
@@ -13,12 +13,14 @@ You can declare the model used for the response with the parameter `response_mod
 !!! note
     Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your path operation function, like all the parameters and body.
 
-It receives a standard Pydantic model and will:
+It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`.
 
-* Convert the output data to the type declarations of the model
-* Validate the data
-* Add a JSON Schema for the response, in the OpenAPI path operation
-* Will be used by the automatic documentation systems
+FastAPI will use this `response_model` to:
+
+* Convert the output data to its type declaration.
+* Validate the data.
+* Add a JSON Schema for the response, in the OpenAPI path operation.
+* Will be used by the automatic documentation systems.
 
 But most importantly:
 
@@ -45,7 +47,7 @@ Now, whenever a browser is creating a user with a password, the API will return
 
 In this case, it might not be a problem, because the user himself is sending the password.
 
-But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
+But if we use the same model for another path operation, we could be sending our user's passwords to every client.
 
 !!! danger
     Never send the plain password of a user in a response.
@@ -84,7 +86,7 @@ And both models will be used for the interactive API documentation:
 
 ## Response Model encoding parameters
 
-If your response model has default values, like:
+Your response model could have default values, like:
 
 ```Python hl_lines="11 13 14"
 {!./src/response_model/tutorial004.py!}
@@ -94,6 +96,12 @@ If your response model has default values, like:
 * `tax: float = None` has a default of `None`.
 * `tags: List[str] = []` has a default of an empty list: `[]`.
 
+but you might want to omit them from the result if they were not actually stored.
+
+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.
+
+### Use the `response_model_skip_defaults` parameter
+
 You can set the *path operation decorator* parameter `response_model_skip_defaults=True`:
 
 ```Python hl_lines="24"
@@ -114,7 +122,7 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t
 !!! 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
+#### 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`:
 
@@ -129,7 +137,7 @@ But if your data has values for the model's fields with default values, like the
 
 they will be included in the response.
 
-### Data with the same values as the defaults
+#### 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`:
 
@@ -152,34 +160,35 @@ So, they will be included in the JSON response.
 
     They can be a list (`[]`), a `float` of `10.5`, etc.
 
-### Use cases
+### `response_model_include` and `response_model_exclude`
 
-This is very useful in several scenarios.
+You can also use the *path operation decorator* parameters `response_model_include` and `response_model_exclude`.
 
-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.
+They take a `set` of `str` with the name of the attributes to include (omitting the rest) or to exclude (including the rest).
 
-### Using Pydantic's `skip_defaults` directly
+This can be used as a quick shortcut if you have only one Pydantic model and want to remove some data from the output.
 
-You can also use your model's `.dict(skip_defaults=True)` in your code.
+!!! tip
+    But it is still recommended to use the ideas above, using multiple classes, instead of these parameters.
 
-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:
+    This is because the JSON Schema generated in your app's OpenAPI (and the docs) will still be the one for the complete model, even if you use `response_model_include` or `response_model_exclude` to omit some attributes.
 
-```Python hl_lines="31 32 33 34 35"
-{!./src/response_model/tutorial004.py!}
+```Python hl_lines="29 35"
+{!./src/response_model/tutorial005.py!}
 ```
 
 !!! tip
-    It's common to use the HTTP `PUT` operation to update data.
+    The syntax `{"name", "description"}` creates a `set` with those two values.
 
-    In theory, `PUT` should be used to "replace" the entire contents.
+    It is equivalent to `set(["name", "description"])`.
 
-    The less known HTTP `PATCH` operation is also used to update data.
+#### Using `list`s instead of `set`s
 
-    But `PATCH` is expected to be used when *partially* updating data. Instead of *replacing* the entire content.
+If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly:
 
-    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.
+```Python hl_lines="29 35"
+{!./src/response_model/tutorial006.py!}
+```
 
 ## Recap
 
index 3d6071ae68cba5414fa5cdcec7d49e0d9bfdb517..0e35543dcfecbe54615a18479531a790abf0f80a 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, List, Optional, Type, Union
+from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
 
 from fastapi import routing
 from fastapi.openapi.docs import (
@@ -138,6 +138,9 @@ class FastAPI(Starlette):
         deprecated: bool = None,
         methods: List[str] = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -157,6 +160,9 @@ class FastAPI(Starlette):
             deprecated=deprecated,
             methods=methods,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -178,6 +184,9 @@ class FastAPI(Starlette):
         deprecated: bool = None,
         methods: List[str] = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -198,6 +207,9 @@ class FastAPI(Starlette):
                 deprecated=deprecated,
                 methods=methods,
                 operation_id=operation_id,
+                response_model_include=response_model_include,
+                response_model_exclude=response_model_exclude,
+                response_model_by_alias=response_model_by_alias,
                 response_model_skip_defaults=response_model_skip_defaults,
                 include_in_schema=include_in_schema,
                 response_class=response_class,
@@ -250,6 +262,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -267,6 +282,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -287,6 +305,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -304,6 +325,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -324,6 +348,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -341,6 +368,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -361,6 +391,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -377,6 +410,9 @@ class FastAPI(Starlette):
             response_description=response_description,
             responses=responses or {},
             deprecated=deprecated,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             operation_id=operation_id,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
@@ -398,6 +434,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -415,6 +454,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -435,6 +477,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -452,6 +497,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -472,6 +520,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -489,6 +540,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -509,6 +563,9 @@ class FastAPI(Starlette):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -526,6 +583,9 @@ class FastAPI(Starlette):
             responses=responses or {},
             deprecated=deprecated,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
index 25ab19fa343a8070ee028617af73f6f64283787b..f5ff57f618d2b935a28f60f694a4ec2204f8393c 100644 (file)
@@ -16,6 +16,10 @@ def jsonable_encoder(
     custom_encoder: dict = {},
     sqlalchemy_safe: bool = True,
 ) -> Any:
+    if include is not None and not isinstance(include, set):
+        include = set(include)
+    if exclude is not None and not isinstance(exclude, set):
+        exclude = set(exclude)
     if isinstance(obj, BaseModel):
         encoder = getattr(obj.Config, "json_encoders", custom_encoder)
         return jsonable_encoder(
index 2ae6d1e0901aaac3d497546746e009d58a2655bd..26d052c343e0cc659fcb9887dbb8cdfe599df788 100644 (file)
@@ -2,7 +2,7 @@ import asyncio
 import inspect
 import logging
 import re
-from typing import Any, Callable, Dict, List, Optional, Type, Union
+from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
 
 from fastapi import params
 from fastapi.dependencies.models import Dependant
@@ -33,10 +33,22 @@ from starlette.websockets import WebSocket
 
 
 def serialize_response(
-    *, field: Field = None, response: Response, skip_defaults: bool = False
+    *,
+    field: Field = None,
+    response: Response,
+    include: Set[str] = None,
+    exclude: Set[str] = set(),
+    by_alias: bool = True,
+    skip_defaults: bool = False,
 ) -> Any:
 
-    encoded = jsonable_encoder(response, skip_defaults=skip_defaults)
+    encoded = jsonable_encoder(
+        response,
+        include=include,
+        exclude=exclude,
+        by_alias=by_alias,
+        skip_defaults=skip_defaults,
+    )
     if field:
         errors = []
         value, errors_ = field.validate(encoded, {}, loc=("response",))
@@ -46,7 +58,13 @@ def serialize_response(
             errors.extend(errors_)
         if errors:
             raise ValidationError(errors)
-        return jsonable_encoder(value, skip_defaults=skip_defaults)
+        return jsonable_encoder(
+            value,
+            include=include,
+            exclude=exclude,
+            by_alias=by_alias,
+            skip_defaults=skip_defaults,
+        )
     else:
         return encoded
 
@@ -57,7 +75,10 @@ def get_app(
     status_code: int = 200,
     response_class: Type[Response] = JSONResponse,
     response_field: Field = None,
-    skip_defaults: bool = False,
+    response_model_include: Set[str] = None,
+    response_model_exclude: Set[str] = set(),
+    response_model_by_alias: bool = True,
+    response_model_skip_defaults: bool = False,
 ) -> Callable:
     assert dependant.call is not None, "dependant.call must be a function"
     is_coroutine = asyncio.iscoroutinefunction(dependant.call)
@@ -97,7 +118,12 @@ def get_app(
                     raw_response.background = background_tasks
                 return raw_response
             response_data = serialize_response(
-                field=response_field, response=raw_response, skip_defaults=skip_defaults
+                field=response_field,
+                response=raw_response,
+                include=response_model_include,
+                exclude=response_model_exclude,
+                by_alias=response_model_by_alias,
+                skip_defaults=response_model_skip_defaults,
             )
             return response_class(
                 content=response_data,
@@ -155,6 +181,9 @@ class APIRoute(routing.Route):
         name: str = None,
         methods: List[str] = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -215,6 +244,9 @@ class APIRoute(routing.Route):
             methods = ["GET"]
         self.methods = methods
         self.operation_id = operation_id
+        self.response_model_include = response_model_include
+        self.response_model_exclude = response_model_exclude
+        self.response_model_by_alias = response_model_by_alias
         self.response_model_skip_defaults = response_model_skip_defaults
         self.include_in_schema = include_in_schema
         self.response_class = response_class
@@ -236,7 +268,10 @@ 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,
+                response_model_include=self.response_model_include,
+                response_model_exclude=self.response_model_exclude,
+                response_model_by_alias=self.response_model_by_alias,
+                response_model_skip_defaults=self.response_model_skip_defaults,
             )
         )
 
@@ -258,6 +293,9 @@ class APIRouter(routing.Router):
         deprecated: bool = None,
         methods: List[str] = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -277,6 +315,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=methods,
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -299,6 +340,9 @@ class APIRouter(routing.Router):
         deprecated: bool = None,
         methods: List[str] = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -319,6 +363,9 @@ class APIRouter(routing.Router):
                 deprecated=deprecated,
                 methods=methods,
                 operation_id=operation_id,
+                response_model_include=response_model_include,
+                response_model_exclude=response_model_exclude,
+                response_model_by_alias=response_model_by_alias,
                 response_model_skip_defaults=response_model_skip_defaults,
                 include_in_schema=include_in_schema,
                 response_class=response_class,
@@ -374,6 +421,9 @@ class APIRouter(routing.Router):
                     deprecated=route.deprecated,
                     methods=route.methods,
                     operation_id=route.operation_id,
+                    response_model_include=route.response_model_include,
+                    response_model_exclude=route.response_model_exclude,
+                    response_model_by_alias=route.response_model_by_alias,
                     response_model_skip_defaults=route.response_model_skip_defaults,
                     include_in_schema=route.include_in_schema,
                     response_class=route.response_class,
@@ -410,6 +460,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -429,6 +482,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["GET"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -449,6 +505,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -467,6 +526,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["PUT"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -487,6 +549,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -505,6 +570,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["POST"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -525,6 +593,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -543,6 +614,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["DELETE"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -563,6 +637,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -581,6 +658,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["OPTIONS"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -601,6 +681,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -619,6 +702,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["HEAD"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -639,6 +725,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -657,6 +746,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["PATCH"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
@@ -677,6 +769,9 @@ class APIRouter(routing.Router):
         responses: Dict[Union[int, str], Dict[str, Any]] = None,
         deprecated: bool = None,
         operation_id: str = None,
+        response_model_include: Set[str] = None,
+        response_model_exclude: Set[str] = set(),
+        response_model_by_alias: bool = True,
         response_model_skip_defaults: bool = False,
         include_in_schema: bool = True,
         response_class: Type[Response] = JSONResponse,
@@ -695,6 +790,9 @@ class APIRouter(routing.Router):
             deprecated=deprecated,
             methods=["TRACE"],
             operation_id=operation_id,
+            response_model_include=response_model_include,
+            response_model_exclude=response_model_exclude,
+            response_model_by_alias=response_model_by_alias,
             response_model_skip_defaults=response_model_skip_defaults,
             include_in_schema=include_in_schema,
             response_class=response_class,
index 8fa41d01257ed454643122c9a968711f6d06b8c4..6954eb044264affda3fcc0a76019e7913cd7583d 100644 (file)
@@ -45,6 +45,7 @@ nav:
         - Path Operation Advanced Configuration: 'tutorial/path-operation-advanced-configuration.md'
         - Additional Status Codes: 'tutorial/additional-status-codes.md'
         - JSON compatible encoder: 'tutorial/encoder.md'
+        - Body - updates: 'tutorial/body-updates.md'
         - Return a Response directly: 'tutorial/response-directly.md'
         - Custom Response Class: 'tutorial/custom-response.md'
         - Additional Responses in OpenAPI: 'tutorial/additional-responses.md'
diff --git a/tests/test_operations_signatures.py b/tests/test_operations_signatures.py
new file mode 100644 (file)
index 0000000..1a74965
--- /dev/null
@@ -0,0 +1,22 @@
+import inspect
+
+from fastapi import APIRouter, FastAPI
+
+method_names = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
+
+
+def test_signatures_consistency():
+    base_sig = inspect.signature(APIRouter.get)
+    for method_name in method_names:
+        router_method = getattr(APIRouter, method_name)
+        app_method = getattr(FastAPI, method_name)
+        router_sig = inspect.signature(router_method)
+        app_sig = inspect.signature(app_method)
+        param: inspect.Parameter
+        for key, param in base_sig.parameters.items():
+            router_param: inspect.Parameter = router_sig.parameters[key]
+            app_param: inspect.Parameter = app_sig.parameters[key]
+            assert param.annotation == router_param.annotation
+            assert param.annotation == app_param.annotation
+            assert param.default == router_param.default
+            assert param.default == app_param.default
diff --git a/tests/test_tutorial/test_body_updates/__init__.py b/tests/test_tutorial/test_body_updates/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py
new file mode 100644 (file)
index 0000000..76a368c
--- /dev/null
@@ -0,0 +1,162 @@
+from starlette.testclient import TestClient
+
+from body_updates.tutorial001 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",
+                    }
+                ],
+            },
+            "put": {
+                "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__put",
+                "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",
+                "type": "object",
+                "properties": {
+                    "name": {"title": "Name", "type": "string"},
+                    "description": {"title": "Description", "type": "string"},
+                    "price": {"title": "Price", "type": "number"},
+                    "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
+
+
+def test_get():
+    response = client.get("/items/baz")
+    assert response.status_code == 200
+    assert response.json() == {
+        "name": "Baz",
+        "description": None,
+        "price": 50.2,
+        "tax": 10.5,
+        "tags": [],
+    }
+
+
+def test_put():
+    response = client.put(
+        "/items/bar", json={"name": "Barz", "price": 3, "description": None}
+    )
+    assert response.json() == {
+        "name": "Barz",
+        "description": None,
+        "price": 3,
+        "tax": 10.5,
+        "tags": [],
+    }
index 152e291e4af409f005edf56248cd7363268d0dc9..c609d690d5ebbc5408c46bf32ba778450d8f1f70 100644 (file)
@@ -41,47 +41,7 @@ openapi_schema = {
                         "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": {
@@ -163,15 +123,3 @@ 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,
-    }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py
new file mode 100644 (file)
index 0000000..b087395
--- /dev/null
@@ -0,0 +1,142 @@
+from starlette.testclient import TestClient
+
+from response_model.tutorial005 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/items/{item_id}/name": {
+            "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 Name",
+                "operationId": "read_item_name_items__item_id__name_get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item_Id", "type": "string"},
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        },
+        "/items/{item_id}/public": {
+            "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 Public Data",
+                "operationId": "read_item_public_data_items__item_id__public_get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item_Id", "type": "string"},
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        },
+    },
+    "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},
+                },
+            },
+            "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
+
+
+def test_read_item_name():
+    response = client.get("/items/bar/name")
+    assert response.status_code == 200
+    assert response.json() == {"name": "Bar", "description": "The Bar fighters"}
+
+
+def test_read_item_public_data():
+    response = client.get("/items/bar/public")
+    assert response.status_code == 200
+    assert response.json() == {
+        "name": "Bar",
+        "description": "The Bar fighters",
+        "price": 62,
+    }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py
new file mode 100644 (file)
index 0000000..d7d2bce
--- /dev/null
@@ -0,0 +1,142 @@
+from starlette.testclient import TestClient
+
+from response_model.tutorial006 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/items/{item_id}/name": {
+            "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 Name",
+                "operationId": "read_item_name_items__item_id__name_get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item_Id", "type": "string"},
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        },
+        "/items/{item_id}/public": {
+            "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 Public Data",
+                "operationId": "read_item_public_data_items__item_id__public_get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item_Id", "type": "string"},
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        },
+    },
+    "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},
+                },
+            },
+            "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
+
+
+def test_read_item_name():
+    response = client.get("/items/bar/name")
+    assert response.status_code == 200
+    assert response.json() == {"name": "Bar", "description": "The Bar fighters"}
+
+
+def test_read_item_public_data():
+    response = client.get("/items/bar/public")
+    assert response.status_code == 200
+    assert response.json() == {
+        "name": "Bar",
+        "description": "The Bar fighters",
+        "price": 62,
+    }