]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add skip_defaults support for path operations (for #242) (#248)
authorWilliam Hayes <william.s.hayes@gmail.com>
Sat, 25 May 2019 15:35:57 +0000 (11:35 -0400)
committerSebastián Ramírez <tiangolo@gmail.com>
Sat, 25 May 2019 15:35:57 +0000 (19:35 +0400)
docs/src/response_model/tutorial001.py
docs/src/response_model/tutorial004.py [new file with mode: 0644]
docs/tutorial/response-model.md
fastapi/applications.py
fastapi/encoders.py
fastapi/routing.py
tests/test_tutorial/test_response_model/test_tutorial004.py [new file with mode: 0644]

index 86dadcbda4c67ee8f35ac981e1516ee27598442e..4fe9aeb504a256fff82b970605fd2485004947f3 100644 (file)
@@ -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
diff --git a/docs/src/response_model/tutorial004.py b/docs/src/response_model/tutorial004.py
new file mode 100644 (file)
index 0000000..30ad218
--- /dev/null
@@ -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
index 21b885ebbe97f4bdaac88c70eb25251e0c5fd7be..1402a6a8f0860d594def36b304ba4ba17f856314 100644 (file)
@@ -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.
index 7041e91d6edc8e135926bb75f662b3ba832e9049..3d6071ae68cba5414fa5cdcec7d49e0d9bfdb517 100644 (file)
@@ -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,
index fc289c936f69373659cc0b16c6248570a92b666b..25ab19fa343a8070ee028617af73f6f64283787b 100644 (file)
@@ -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,
index c902bb2add42cc927eaae54632f90b093551e4dd..2ae6d1e0901aaac3d497546746e009d58a2655bd 100644 (file)
@@ -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,
diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py
new file mode 100644 (file)
index 0000000..152e291
--- /dev/null
@@ -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,
+    }