]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add support for `openapi_examples` in all FastAPI parameters (#10152)
authorSebastián Ramírez <tiangolo@gmail.com>
Sat, 26 Aug 2023 18:03:13 +0000 (20:03 +0200)
committerGitHub <noreply@github.com>
Sat, 26 Aug 2023 18:03:13 +0000 (20:03 +0200)
* ♻️ Refactor model for OpenAPI Examples to use a reusable TypedDict

* ✨ Add support for openapi_examples in parameters

* 📝 Add new docs examples for new parameter openapi_examples

* 📝 Update docs for Schema Extra to include OpenAPI examples

* ✅ Add tests for new source examples, for openapi_examples

* ✅ Add tests for openapi_examples corner cases and all parameters

* 💡 Tweak and ignore type annotation checks for custom TypedDict

16 files changed:
docs/en/docs/tutorial/schema-extra-example.md
docs_src/schema_extra_example/tutorial005.py [new file with mode: 0644]
docs_src/schema_extra_example/tutorial005_an.py [new file with mode: 0644]
docs_src/schema_extra_example/tutorial005_an_py310.py [new file with mode: 0644]
docs_src/schema_extra_example/tutorial005_an_py39.py [new file with mode: 0644]
docs_src/schema_extra_example/tutorial005_py310.py [new file with mode: 0644]
fastapi/openapi/models.py
fastapi/openapi/utils.py
fastapi/param_functions.py
fastapi/params.py
tests/test_openapi_examples.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py [new file with mode: 0644]

index 39d184763fef3a2565680ea4b1672483b16a9a7b..8cf1b9c091cb4fac6114a9004f0628b31b0224f3 100644 (file)
@@ -74,7 +74,7 @@ When using `Field()` with Pydantic models, you can also declare additional `exam
     {!> ../../../docs_src/schema_extra_example/tutorial002.py!}
     ```
 
-## `examples` in OpenAPI
+## `examples` in JSON Schema - OpenAPI
 
 When using any of:
 
@@ -86,7 +86,7 @@ When using any of:
 * `Form()`
 * `File()`
 
-you can also declare a group of `examples` with additional information that will be added to **OpenAPI**.
+you can also declare a group of `examples` with additional information that will be added to their **JSON Schemas** inside of **OpenAPI**.
 
 ### `Body` with `examples`
 
@@ -174,9 +174,84 @@ You can of course also pass multiple `examples`:
     {!> ../../../docs_src/schema_extra_example/tutorial004.py!}
     ```
 
-### Examples in the docs UI
+When you do this, the examples will be part of the internal **JSON Schema** for that body data.
 
-With `examples` added to `Body()` the `/docs` would look like:
+Nevertheless, at the <abbr title="2023-08-26">time of writing this</abbr>, Swagger UI, the tool in charge of showing the docs UI, doesn't support showing multiple examples for the data in **JSON Schema**. But read below for a workaround.
+
+### OpenAPI-specific `examples`
+
+Since before **JSON Schema** supported `examples` OpenAPI had support for a different field also called `examples`.
+
+This **OpenAPI-specific** `examples` goes in another section in the OpenAPI specification. It goes in the **details for each *path operation***, not inside each JSON Schema.
+
+And Swagger UI has supported this particular `examples` field for a while. So, you can use it to **show** different **examples in the docs UI**.
+
+The shape of this OpenAPI-specific field `examples` is a `dict` with **multiple examples** (instead of a `list`), each with extra information that will be added to **OpenAPI** too.
+
+This doesn't go inside of each JSON Schema contained in OpenAPI, this goes outside, in the *path operation* directly.
+
+### Using the `openapi_examples` Parameter
+
+You can declare the OpenAPI-specific `examples` in FastAPI with the parameter `openapi_examples` for:
+
+* `Path()`
+* `Query()`
+* `Header()`
+* `Cookie()`
+* `Body()`
+* `Form()`
+* `File()`
+
+The keys of the `dict` identify each example, and each value is another `dict`.
+
+Each specific example `dict` in the `examples` can contain:
+
+* `summary`: Short description for the example.
+* `description`: A long description that can contain Markdown text.
+* `value`: This is the actual example shown, e.g. a `dict`.
+* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`.
+
+You can use it like this:
+
+=== "Python 3.10+"
+
+    ```Python hl_lines="23-49"
+    {!> ../../../docs_src/schema_extra_example/tutorial005_an_py310.py!}
+    ```
+
+=== "Python 3.9+"
+
+    ```Python hl_lines="23-49"
+    {!> ../../../docs_src/schema_extra_example/tutorial005_an_py39.py!}
+    ```
+
+=== "Python 3.6+"
+
+    ```Python hl_lines="24-50"
+    {!> ../../../docs_src/schema_extra_example/tutorial005_an.py!}
+    ```
+
+=== "Python 3.10+ non-Annotated"
+
+    !!! tip
+        Prefer to use the `Annotated` version if possible.
+
+    ```Python hl_lines="19-45"
+    {!> ../../../docs_src/schema_extra_example/tutorial005_py310.py!}
+    ```
+
+=== "Python 3.6+ non-Annotated"
+
+    !!! tip
+        Prefer to use the `Annotated` version if possible.
+
+    ```Python hl_lines="21-47"
+    {!> ../../../docs_src/schema_extra_example/tutorial005.py!}
+    ```
+
+### OpenAPI Examples in the Docs UI
+
+With `openapi_examples` added to `Body()` the `/docs` would look like:
 
 <img src="/img/tutorial/body-fields/image02.png">
 
@@ -210,20 +285,8 @@ OpenAPI also added `example` and `examples` fields to other parts of the specifi
     * `File()`
     * `Form()`
 
-### OpenAPI's `examples` field
-
-The shape of this field `examples` from OpenAPI is a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too.
-
-The keys of the `dict` identify each example, and each value is another `dict`.
-
-Each specific example `dict` in the `examples` can contain:
-
-* `summary`: Short description for the example.
-* `description`: A long description that can contain Markdown text.
-* `value`: This is the actual example shown, e.g. a `dict`.
-* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`.
-
-This applies to those other parts of the OpenAPI specification apart from JSON Schema.
+!!! info
+    This old OpenAPI-specific `examples` parameter is now `openapi_examples` since FastAPI `0.103.0`.
 
 ### JSON Schema's `examples` field
 
@@ -250,6 +313,12 @@ In versions of FastAPI before 0.99.0 (0.99.0 and above use the newer OpenAPI 3.1
 
 But now that FastAPI 0.99.0 and above uses OpenAPI 3.1.0, that uses JSON Schema 2020-12, and Swagger UI 5.0.0 and above, everything is more consistent and the examples are included in JSON Schema.
 
+### Swagger UI and OpenAPI-specific `examples`
+
+Now, as Swagger UI didn't support multiple JSON Schema examples (as of 2023-08-26), users didn't have a way to show multiple examples in the docs.
+
+To solve that, FastAPI `0.103.0` **added support** for declaring the same old **OpenAPI-specific** `examples` field with the new parameter `openapi_examples`. 🤓
+
 ### Summary
 
 I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅
diff --git a/docs_src/schema_extra_example/tutorial005.py b/docs_src/schema_extra_example/tutorial005.py
new file mode 100644 (file)
index 0000000..b8217c2
--- /dev/null
@@ -0,0 +1,51 @@
+from typing import Union
+
+from fastapi import Body, FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    price: float
+    tax: Union[float, None] = None
+
+
+@app.put("/items/{item_id}")
+async def update_item(
+    *,
+    item_id: int,
+    item: Item = Body(
+        openapi_examples={
+            "normal": {
+                "summary": "A normal example",
+                "description": "A **normal** item works correctly.",
+                "value": {
+                    "name": "Foo",
+                    "description": "A very nice Item",
+                    "price": 35.4,
+                    "tax": 3.2,
+                },
+            },
+            "converted": {
+                "summary": "An example with converted data",
+                "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                "value": {
+                    "name": "Bar",
+                    "price": "35.4",
+                },
+            },
+            "invalid": {
+                "summary": "Invalid data is rejected with an error",
+                "value": {
+                    "name": "Baz",
+                    "price": "thirty five point four",
+                },
+            },
+        },
+    ),
+):
+    results = {"item_id": item_id, "item": item}
+    return results
diff --git a/docs_src/schema_extra_example/tutorial005_an.py b/docs_src/schema_extra_example/tutorial005_an.py
new file mode 100644 (file)
index 0000000..4b2d9c6
--- /dev/null
@@ -0,0 +1,55 @@
+from typing import Union
+
+from fastapi import Body, FastAPI
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    price: float
+    tax: Union[float, None] = None
+
+
+@app.put("/items/{item_id}")
+async def update_item(
+    *,
+    item_id: int,
+    item: Annotated[
+        Item,
+        Body(
+            openapi_examples={
+                "normal": {
+                    "summary": "A normal example",
+                    "description": "A **normal** item works correctly.",
+                    "value": {
+                        "name": "Foo",
+                        "description": "A very nice Item",
+                        "price": 35.4,
+                        "tax": 3.2,
+                    },
+                },
+                "converted": {
+                    "summary": "An example with converted data",
+                    "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                    "value": {
+                        "name": "Bar",
+                        "price": "35.4",
+                    },
+                },
+                "invalid": {
+                    "summary": "Invalid data is rejected with an error",
+                    "value": {
+                        "name": "Baz",
+                        "price": "thirty five point four",
+                    },
+                },
+            },
+        ),
+    ],
+):
+    results = {"item_id": item_id, "item": item}
+    return results
diff --git a/docs_src/schema_extra_example/tutorial005_an_py310.py b/docs_src/schema_extra_example/tutorial005_an_py310.py
new file mode 100644 (file)
index 0000000..64dc2cf
--- /dev/null
@@ -0,0 +1,54 @@
+from typing import Annotated
+
+from fastapi import Body, FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    price: float
+    tax: float | None = None
+
+
+@app.put("/items/{item_id}")
+async def update_item(
+    *,
+    item_id: int,
+    item: Annotated[
+        Item,
+        Body(
+            openapi_examples={
+                "normal": {
+                    "summary": "A normal example",
+                    "description": "A **normal** item works correctly.",
+                    "value": {
+                        "name": "Foo",
+                        "description": "A very nice Item",
+                        "price": 35.4,
+                        "tax": 3.2,
+                    },
+                },
+                "converted": {
+                    "summary": "An example with converted data",
+                    "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                    "value": {
+                        "name": "Bar",
+                        "price": "35.4",
+                    },
+                },
+                "invalid": {
+                    "summary": "Invalid data is rejected with an error",
+                    "value": {
+                        "name": "Baz",
+                        "price": "thirty five point four",
+                    },
+                },
+            },
+        ),
+    ],
+):
+    results = {"item_id": item_id, "item": item}
+    return results
diff --git a/docs_src/schema_extra_example/tutorial005_an_py39.py b/docs_src/schema_extra_example/tutorial005_an_py39.py
new file mode 100644 (file)
index 0000000..edeb1af
--- /dev/null
@@ -0,0 +1,54 @@
+from typing import Annotated, Union
+
+from fastapi import Body, FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    price: float
+    tax: Union[float, None] = None
+
+
+@app.put("/items/{item_id}")
+async def update_item(
+    *,
+    item_id: int,
+    item: Annotated[
+        Item,
+        Body(
+            openapi_examples={
+                "normal": {
+                    "summary": "A normal example",
+                    "description": "A **normal** item works correctly.",
+                    "value": {
+                        "name": "Foo",
+                        "description": "A very nice Item",
+                        "price": 35.4,
+                        "tax": 3.2,
+                    },
+                },
+                "converted": {
+                    "summary": "An example with converted data",
+                    "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                    "value": {
+                        "name": "Bar",
+                        "price": "35.4",
+                    },
+                },
+                "invalid": {
+                    "summary": "Invalid data is rejected with an error",
+                    "value": {
+                        "name": "Baz",
+                        "price": "thirty five point four",
+                    },
+                },
+            },
+        ),
+    ],
+):
+    results = {"item_id": item_id, "item": item}
+    return results
diff --git a/docs_src/schema_extra_example/tutorial005_py310.py b/docs_src/schema_extra_example/tutorial005_py310.py
new file mode 100644 (file)
index 0000000..eef9733
--- /dev/null
@@ -0,0 +1,49 @@
+from fastapi import Body, FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    price: float
+    tax: float | None = None
+
+
+@app.put("/items/{item_id}")
+async def update_item(
+    *,
+    item_id: int,
+    item: Item = Body(
+        openapi_examples={
+            "normal": {
+                "summary": "A normal example",
+                "description": "A **normal** item works correctly.",
+                "value": {
+                    "name": "Foo",
+                    "description": "A very nice Item",
+                    "price": 35.4,
+                    "tax": 3.2,
+                },
+            },
+            "converted": {
+                "summary": "An example with converted data",
+                "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                "value": {
+                    "name": "Bar",
+                    "price": "35.4",
+                },
+            },
+            "invalid": {
+                "summary": "Invalid data is rejected with an error",
+                "value": {
+                    "name": "Baz",
+                    "price": "thirty five point four",
+                },
+            },
+        },
+    ),
+):
+    results = {"item_id": item_id, "item": item}
+    return results
index 2268dd229091d10dd0535bd21515b40409b8ce1b..3d982eb9aec98b6ba3bdb370f1271ce461e25031 100644 (file)
@@ -11,7 +11,7 @@ from fastapi._compat import (
 )
 from fastapi.logger import logger
 from pydantic import AnyUrl, BaseModel, Field
-from typing_extensions import Annotated, Literal
+from typing_extensions import Annotated, Literal, TypedDict
 from typing_extensions import deprecated as typing_deprecated
 
 try:
@@ -267,14 +267,14 @@ class Schema(BaseModel):
 SchemaOrBool = Union[Schema, bool]
 
 
-class Example(BaseModel):
-    summary: Optional[str] = None
-    description: Optional[str] = None
-    value: Optional[Any] = None
-    externalValue: Optional[AnyUrl] = None
+class Example(TypedDict, total=False):
+    summary: Optional[str]
+    description: Optional[str]
+    value: Optional[Any]
+    externalValue: Optional[AnyUrl]
 
-    if PYDANTIC_V2:
-        model_config = {"extra": "allow"}
+    if PYDANTIC_V2:  # type: ignore [misc]
+        __pydantic_config__ = {"extra": "allow"}
 
     else:
 
index 9498375fefa081bd38a74ee35773027a60e78460..5bfb5acef7fc47b220bfb159345b5c02cd8e8398 100644 (file)
@@ -118,7 +118,9 @@ def get_openapi_operation_parameters(
         }
         if field_info.description:
             parameter["description"] = field_info.description
-        if field_info.example != Undefined:
+        if field_info.openapi_examples:
+            parameter["examples"] = jsonable_encoder(field_info.openapi_examples)
+        elif field_info.example != Undefined:
             parameter["example"] = jsonable_encoder(field_info.example)
         if field_info.deprecated:
             parameter["deprecated"] = field_info.deprecated
@@ -153,7 +155,11 @@ def get_openapi_operation_request_body(
     if required:
         request_body_oai["required"] = required
     request_media_content: Dict[str, Any] = {"schema": body_schema}
-    if field_info.example != Undefined:
+    if field_info.openapi_examples:
+        request_media_content["examples"] = jsonable_encoder(
+            field_info.openapi_examples
+        )
+    elif field_info.example != Undefined:
         request_media_content["example"] = jsonable_encoder(field_info.example)
     request_body_oai["content"] = {request_media_type: request_media_content}
     return request_body_oai
index a43afaf311798ebde5fb265e1d47d584d807152d..63914d1d68ff43594d0e765606ec0e80819d9432 100644 (file)
@@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
 
 from fastapi import params
 from fastapi._compat import Undefined
+from fastapi.openapi.models import Example
 from typing_extensions import Annotated, deprecated
 
 _Unset: Any = Undefined
@@ -46,6 +47,7 @@ def Path(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -76,6 +78,7 @@ def Path(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -122,6 +125,7 @@ def Query(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -152,6 +156,7 @@ def Query(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -199,6 +204,7 @@ def Header(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -230,6 +236,7 @@ def Header(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -276,6 +283,7 @@ def Cookie(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -306,6 +314,7 @@ def Cookie(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -354,6 +363,7 @@ def Body(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -386,6 +396,7 @@ def Body(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -433,6 +444,7 @@ def Form(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -464,6 +476,7 @@ def Form(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
@@ -511,6 +524,7 @@ def File(  # noqa: N802
             "although still supported. Use examples instead."
         ),
     ] = _Unset,
+    openapi_examples: Optional[Dict[str, Example]] = None,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
     json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -542,6 +556,7 @@ def File(  # noqa: N802
         decimal_places=decimal_places,
         example=example,
         examples=examples,
+        openapi_examples=openapi_examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
         json_schema_extra=json_schema_extra,
index 2d8100650e49202758b6f906ae0e7125bbee3600..b40944dba6f56498205bfe899d8054e3fcd2e6e3 100644 (file)
@@ -2,6 +2,7 @@ import warnings
 from enum import Enum
 from typing import Any, Callable, Dict, List, Optional, Sequence, Union
 
+from fastapi.openapi.models import Example
 from pydantic.fields import FieldInfo
 from typing_extensions import Annotated, deprecated
 
@@ -61,6 +62,7 @@ class Param(FieldInfo):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -75,6 +77,7 @@ class Param(FieldInfo):
             )
         self.example = example
         self.include_in_schema = include_in_schema
+        self.openapi_examples = openapi_examples
         kwargs = dict(
             default=default,
             default_factory=default_factory,
@@ -170,6 +173,7 @@ class Path(Param):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -204,6 +208,7 @@ class Path(Param):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
@@ -254,6 +259,7 @@ class Query(Param):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -286,6 +292,7 @@ class Query(Param):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
@@ -337,6 +344,7 @@ class Header(Param):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -370,6 +378,7 @@ class Header(Param):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
@@ -420,6 +429,7 @@ class Cookie(Param):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -452,6 +462,7 @@ class Cookie(Param):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
@@ -502,6 +513,7 @@ class Body(FieldInfo):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -518,6 +530,7 @@ class Body(FieldInfo):
             )
         self.example = example
         self.include_in_schema = include_in_schema
+        self.openapi_examples = openapi_examples
         kwargs = dict(
             default=default,
             default_factory=default_factory,
@@ -613,6 +626,7 @@ class Form(Body):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -647,6 +661,7 @@ class Form(Body):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
@@ -696,6 +711,7 @@ class File(Form):
                 "although still supported. Use examples instead."
             ),
         ] = _Unset,
+        openapi_examples: Optional[Dict[str, Example]] = None,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
         json_schema_extra: Union[Dict[str, Any], None] = None,
@@ -729,6 +745,7 @@ class File(Form):
             deprecated=deprecated,
             example=example,
             examples=examples,
+            openapi_examples=openapi_examples,
             include_in_schema=include_in_schema,
             json_schema_extra=json_schema_extra,
             **extra,
diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py
new file mode 100644 (file)
index 0000000..d0e3595
--- /dev/null
@@ -0,0 +1,455 @@
+from typing import Union
+
+from dirty_equals import IsDict
+from fastapi import Body, Cookie, FastAPI, Header, Path, Query
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    data: str
+
+
+@app.post("/examples/")
+def examples(
+    item: Item = Body(
+        examples=[
+            {"data": "Data in Body examples, example1"},
+        ],
+        openapi_examples={
+            "Example One": {
+                "summary": "Example One Summary",
+                "description": "Example One Description",
+                "value": {"data": "Data in Body examples, example1"},
+            },
+            "Example Two": {
+                "value": {"data": "Data in Body examples, example2"},
+            },
+        },
+    )
+):
+    return item
+
+
+@app.get("/path_examples/{item_id}")
+def path_examples(
+    item_id: str = Path(
+        examples=[
+            "json_schema_item_1",
+            "json_schema_item_2",
+        ],
+        openapi_examples={
+            "Path One": {
+                "summary": "Path One Summary",
+                "description": "Path One Description",
+                "value": "item_1",
+            },
+            "Path Two": {
+                "value": "item_2",
+            },
+        },
+    ),
+):
+    return item_id
+
+
+@app.get("/query_examples/")
+def query_examples(
+    data: Union[str, None] = Query(
+        default=None,
+        examples=[
+            "json_schema_query1",
+            "json_schema_query2",
+        ],
+        openapi_examples={
+            "Query One": {
+                "summary": "Query One Summary",
+                "description": "Query One Description",
+                "value": "query1",
+            },
+            "Query Two": {
+                "value": "query2",
+            },
+        },
+    ),
+):
+    return data
+
+
+@app.get("/header_examples/")
+def header_examples(
+    data: Union[str, None] = Header(
+        default=None,
+        examples=[
+            "json_schema_header1",
+            "json_schema_header2",
+        ],
+        openapi_examples={
+            "Header One": {
+                "summary": "Header One Summary",
+                "description": "Header One Description",
+                "value": "header1",
+            },
+            "Header Two": {
+                "value": "header2",
+            },
+        },
+    ),
+):
+    return data
+
+
+@app.get("/cookie_examples/")
+def cookie_examples(
+    data: Union[str, None] = Cookie(
+        default=None,
+        examples=["json_schema_cookie1", "json_schema_cookie2"],
+        openapi_examples={
+            "Cookie One": {
+                "summary": "Cookie One Summary",
+                "description": "Cookie One Description",
+                "value": "cookie1",
+            },
+            "Cookie Two": {
+                "value": "cookie2",
+            },
+        },
+    ),
+):
+    return data
+
+
+client = TestClient(app)
+
+
+def test_call_api():
+    response = client.get("/path_examples/foo")
+    assert response.status_code == 200, response.text
+
+    response = client.get("/query_examples/")
+    assert response.status_code == 200, response.text
+
+    response = client.get("/header_examples/")
+    assert response.status_code == 200, response.text
+
+    response = client.get("/cookie_examples/")
+    assert response.status_code == 200, response.text
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/examples/": {
+                "post": {
+                    "summary": "Examples",
+                    "operationId": "examples_examples__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
+                                    "title": "Item",
+                                    "examples": [
+                                        {"data": "Data in Body examples, example1"}
+                                    ],
+                                },
+                                "examples": {
+                                    "Example One": {
+                                        "summary": "Example One Summary",
+                                        "description": "Example One Description",
+                                        "value": {
+                                            "data": "Data in Body examples, example1"
+                                        },
+                                    },
+                                    "Example Two": {
+                                        "value": {
+                                            "data": "Data in Body examples, example2"
+                                        }
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+            "/path_examples/{item_id}": {
+                "get": {
+                    "summary": "Path Examples",
+                    "operationId": "path_examples_path_examples__item_id__get",
+                    "parameters": [
+                        {
+                            "name": "item_id",
+                            "in": "path",
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "examples": [
+                                    "json_schema_item_1",
+                                    "json_schema_item_2",
+                                ],
+                                "title": "Item Id",
+                            },
+                            "examples": {
+                                "Path One": {
+                                    "summary": "Path One Summary",
+                                    "description": "Path One Description",
+                                    "value": "item_1",
+                                },
+                                "Path Two": {"value": "item_2"},
+                            },
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+            "/query_examples/": {
+                "get": {
+                    "summary": "Query Examples",
+                    "operationId": "query_examples_query_examples__get",
+                    "parameters": [
+                        {
+                            "name": "data",
+                            "in": "query",
+                            "required": False,
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "examples": [
+                                        "json_schema_query1",
+                                        "json_schema_query2",
+                                    ],
+                                    "title": "Data",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "examples": [
+                                        "json_schema_query1",
+                                        "json_schema_query2",
+                                    ],
+                                    "type": "string",
+                                    "title": "Data",
+                                }
+                            ),
+                            "examples": {
+                                "Query One": {
+                                    "summary": "Query One Summary",
+                                    "description": "Query One Description",
+                                    "value": "query1",
+                                },
+                                "Query Two": {"value": "query2"},
+                            },
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+            "/header_examples/": {
+                "get": {
+                    "summary": "Header Examples",
+                    "operationId": "header_examples_header_examples__get",
+                    "parameters": [
+                        {
+                            "name": "data",
+                            "in": "header",
+                            "required": False,
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "examples": [
+                                        "json_schema_header1",
+                                        "json_schema_header2",
+                                    ],
+                                    "title": "Data",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "examples": [
+                                        "json_schema_header1",
+                                        "json_schema_header2",
+                                    ],
+                                    "title": "Data",
+                                }
+                            ),
+                            "examples": {
+                                "Header One": {
+                                    "summary": "Header One Summary",
+                                    "description": "Header One Description",
+                                    "value": "header1",
+                                },
+                                "Header Two": {"value": "header2"},
+                            },
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+            "/cookie_examples/": {
+                "get": {
+                    "summary": "Cookie Examples",
+                    "operationId": "cookie_examples_cookie_examples__get",
+                    "parameters": [
+                        {
+                            "name": "data",
+                            "in": "cookie",
+                            "required": False,
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "examples": [
+                                        "json_schema_cookie1",
+                                        "json_schema_cookie2",
+                                    ],
+                                    "title": "Data",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "examples": [
+                                        "json_schema_cookie1",
+                                        "json_schema_cookie2",
+                                    ],
+                                    "title": "Data",
+                                }
+                            ),
+                            "examples": {
+                                "Cookie One": {
+                                    "summary": "Cookie One Summary",
+                                    "description": "Cookie One Description",
+                                    "value": "cookie1",
+                                },
+                                "Cookie Two": {"value": "cookie2"},
+                            },
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {"data": {"type": "string", "title": "Data"}},
+                    "type": "object",
+                    "required": ["data"],
+                    "title": "Item",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py
new file mode 100644 (file)
index 0000000..94a40ed
--- /dev/null
@@ -0,0 +1,166 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial005 import app
+
+    client = TestClient(app)
+    return client
+
+
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+def test_openapi_schema(client: TestClient) -> None:
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": IsDict({"$ref": "#/components/schemas/Item"})
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                                "examples": {
+                                    "normal": {
+                                        "summary": "A normal example",
+                                        "description": "A **normal** item works correctly.",
+                                        "value": {
+                                            "name": "Foo",
+                                            "description": "A very nice Item",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        },
+                                    },
+                                    "converted": {
+                                        "summary": "An example with converted data",
+                                        "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                                        "value": {"name": "Bar", "price": "35.4"},
+                                    },
+                                    "invalid": {
+                                        "summary": "Invalid data is rejected with an error",
+                                        "value": {
+                                            "name": "Baz",
+                                            "price": "thirty five point four",
+                                        },
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": IsDict(
+                            {
+                                "title": "Description",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Description", "type": "string"}
+                        ),
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": IsDict(
+                            {
+                                "title": "Tax",
+                                "anyOf": [{"type": "number"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Tax", "type": "number"}
+                        ),
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py
new file mode 100644 (file)
index 0000000..da92f98
--- /dev/null
@@ -0,0 +1,166 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial005_an import app
+
+    client = TestClient(app)
+    return client
+
+
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+def test_openapi_schema(client: TestClient) -> None:
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": IsDict({"$ref": "#/components/schemas/Item"})
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                                "examples": {
+                                    "normal": {
+                                        "summary": "A normal example",
+                                        "description": "A **normal** item works correctly.",
+                                        "value": {
+                                            "name": "Foo",
+                                            "description": "A very nice Item",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        },
+                                    },
+                                    "converted": {
+                                        "summary": "An example with converted data",
+                                        "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                                        "value": {"name": "Bar", "price": "35.4"},
+                                    },
+                                    "invalid": {
+                                        "summary": "Invalid data is rejected with an error",
+                                        "value": {
+                                            "name": "Baz",
+                                            "price": "thirty five point four",
+                                        },
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": IsDict(
+                            {
+                                "title": "Description",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Description", "type": "string"}
+                        ),
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": IsDict(
+                            {
+                                "title": "Tax",
+                                "anyOf": [{"type": "number"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Tax", "type": "number"}
+                        ),
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py
new file mode 100644 (file)
index 0000000..9109cb1
--- /dev/null
@@ -0,0 +1,170 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial005_an_py310 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient) -> None:
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": IsDict({"$ref": "#/components/schemas/Item"})
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                                "examples": {
+                                    "normal": {
+                                        "summary": "A normal example",
+                                        "description": "A **normal** item works correctly.",
+                                        "value": {
+                                            "name": "Foo",
+                                            "description": "A very nice Item",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        },
+                                    },
+                                    "converted": {
+                                        "summary": "An example with converted data",
+                                        "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                                        "value": {"name": "Bar", "price": "35.4"},
+                                    },
+                                    "invalid": {
+                                        "summary": "Invalid data is rejected with an error",
+                                        "value": {
+                                            "name": "Baz",
+                                            "price": "thirty five point four",
+                                        },
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": IsDict(
+                            {
+                                "title": "Description",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Description", "type": "string"}
+                        ),
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": IsDict(
+                            {
+                                "title": "Tax",
+                                "anyOf": [{"type": "number"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Tax", "type": "number"}
+                        ),
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py
new file mode 100644 (file)
index 0000000..fd4ec05
--- /dev/null
@@ -0,0 +1,170 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py39
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial005_an_py39 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py39
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_py39
+def test_openapi_schema(client: TestClient) -> None:
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": IsDict({"$ref": "#/components/schemas/Item"})
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                                "examples": {
+                                    "normal": {
+                                        "summary": "A normal example",
+                                        "description": "A **normal** item works correctly.",
+                                        "value": {
+                                            "name": "Foo",
+                                            "description": "A very nice Item",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        },
+                                    },
+                                    "converted": {
+                                        "summary": "An example with converted data",
+                                        "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                                        "value": {"name": "Bar", "price": "35.4"},
+                                    },
+                                    "invalid": {
+                                        "summary": "Invalid data is rejected with an error",
+                                        "value": {
+                                            "name": "Baz",
+                                            "price": "thirty five point four",
+                                        },
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": IsDict(
+                            {
+                                "title": "Description",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Description", "type": "string"}
+                        ),
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": IsDict(
+                            {
+                                "title": "Tax",
+                                "anyOf": [{"type": "number"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Tax", "type": "number"}
+                        ),
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py
new file mode 100644 (file)
index 0000000..05df534
--- /dev/null
@@ -0,0 +1,170 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial005_py310 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient) -> None:
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": IsDict({"$ref": "#/components/schemas/Item"})
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                    }
+                                ),
+                                "examples": {
+                                    "normal": {
+                                        "summary": "A normal example",
+                                        "description": "A **normal** item works correctly.",
+                                        "value": {
+                                            "name": "Foo",
+                                            "description": "A very nice Item",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        },
+                                    },
+                                    "converted": {
+                                        "summary": "An example with converted data",
+                                        "description": "FastAPI can convert price `strings` to actual `numbers` automatically",
+                                        "value": {"name": "Bar", "price": "35.4"},
+                                    },
+                                    "invalid": {
+                                        "summary": "Invalid data is rejected with an error",
+                                        "value": {
+                                            "name": "Baz",
+                                            "price": "thirty five point four",
+                                        },
+                                    },
+                                },
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": IsDict(
+                            {
+                                "title": "Description",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Description", "type": "string"}
+                        ),
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": IsDict(
+                            {
+                                "title": "Tax",
+                                "anyOf": [{"type": "number"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Tax", "type": "number"}
+                        ),
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }