diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md
index 39d184763..8cf1b9c09 100644
--- a/docs/en/docs/tutorial/schema-extra-example.md
+++ b/docs/en/docs/tutorial/schema-extra-example.md
@@ -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 time of writing this, 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:
@@ -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
index 000000000..b8217c27e
--- /dev/null
+++ b/docs_src/schema_extra_example/tutorial005.py
@@ -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
index 000000000..4b2d9c662
--- /dev/null
+++ b/docs_src/schema_extra_example/tutorial005_an.py
@@ -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
index 000000000..64dc2cf90
--- /dev/null
+++ b/docs_src/schema_extra_example/tutorial005_an_py310.py
@@ -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
index 000000000..edeb1affc
--- /dev/null
+++ b/docs_src/schema_extra_example/tutorial005_an_py39.py
@@ -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
index 000000000..eef973343
--- /dev/null
+++ b/docs_src/schema_extra_example/tutorial005_py310.py
@@ -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
diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py
index 2268dd229..3d982eb9a 100644
--- a/fastapi/openapi/models.py
+++ b/fastapi/openapi/models.py
@@ -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:
diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py
index 9498375fe..5bfb5acef 100644
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -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
diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py
index a43afaf31..63914d1d6 100644
--- a/fastapi/param_functions.py
+++ b/fastapi/param_functions.py
@@ -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,
diff --git a/fastapi/params.py b/fastapi/params.py
index 2d8100650..b40944dba 100644
--- a/fastapi/params.py
+++ b/fastapi/params.py
@@ -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
index 000000000..d0e35953e
--- /dev/null
+++ b/tests/test_openapi_examples.py
@@ -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
index 000000000..94a40ed5a
--- /dev/null
+++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py
@@ -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
index 000000000..da92f98f6
--- /dev/null
+++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py
@@ -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
index 000000000..9109cb14e
--- /dev/null
+++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py
@@ -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
index 000000000..fd4ec0575
--- /dev/null
+++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py
@@ -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
index 000000000..05df53422
--- /dev/null
+++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py
@@ -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"},
+ },
+ },
+ }
+ },
+ }