Browse Source

Add support for `openapi_examples` in all FastAPI parameters (#10152)

* ♻️ 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
pull/10164/head
Sebastián Ramírez 2 years ago
committed by GitHub
parent
commit
1b714b3177
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 105
      docs/en/docs/tutorial/schema-extra-example.md
  2. 51
      docs_src/schema_extra_example/tutorial005.py
  3. 55
      docs_src/schema_extra_example/tutorial005_an.py
  4. 54
      docs_src/schema_extra_example/tutorial005_an_py310.py
  5. 54
      docs_src/schema_extra_example/tutorial005_an_py39.py
  6. 49
      docs_src/schema_extra_example/tutorial005_py310.py
  7. 16
      fastapi/openapi/models.py
  8. 10
      fastapi/openapi/utils.py
  9. 15
      fastapi/param_functions.py
  10. 17
      fastapi/params.py
  11. 455
      tests/test_openapi_examples.py
  12. 166
      tests/test_tutorial/test_schema_extra_example/test_tutorial005.py
  13. 166
      tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py
  14. 170
      tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py
  15. 170
      tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py
  16. 170
      tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py

105
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!} {!> ../../../docs_src/schema_extra_example/tutorial002.py!}
``` ```
## `examples` in OpenAPI ## `examples` in JSON Schema - OpenAPI
When using any of: When using any of:
@ -86,7 +86,7 @@ When using any of:
* `Form()` * `Form()`
* `File()` * `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` ### `Body` with `examples`
@ -174,9 +174,84 @@ You can of course also pass multiple `examples`:
{!> ../../../docs_src/schema_extra_example/tutorial004.py!} {!> ../../../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"> <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()` * `File()`
* `Form()` * `Form()`
### OpenAPI's `examples` field !!! info
This old OpenAPI-specific `examples` parameter is now `openapi_examples` since FastAPI `0.103.0`.
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.
### JSON Schema's `examples` field ### 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. 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 ### Summary
I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅 I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅

51
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

55
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

54
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

54
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

49
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

16
fastapi/openapi/models.py

@ -11,7 +11,7 @@ from fastapi._compat import (
) )
from fastapi.logger import logger from fastapi.logger import logger
from pydantic import AnyUrl, BaseModel, Field 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 from typing_extensions import deprecated as typing_deprecated
try: try:
@ -267,14 +267,14 @@ class Schema(BaseModel):
SchemaOrBool = Union[Schema, bool] SchemaOrBool = Union[Schema, bool]
class Example(BaseModel): class Example(TypedDict, total=False):
summary: Optional[str] = None summary: Optional[str]
description: Optional[str] = None description: Optional[str]
value: Optional[Any] = None value: Optional[Any]
externalValue: Optional[AnyUrl] = None externalValue: Optional[AnyUrl]
if PYDANTIC_V2: if PYDANTIC_V2: # type: ignore [misc]
model_config = {"extra": "allow"} __pydantic_config__ = {"extra": "allow"}
else: else:

10
fastapi/openapi/utils.py

@ -118,7 +118,9 @@ def get_openapi_operation_parameters(
} }
if field_info.description: if field_info.description:
parameter["description"] = 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) parameter["example"] = jsonable_encoder(field_info.example)
if field_info.deprecated: if field_info.deprecated:
parameter["deprecated"] = field_info.deprecated parameter["deprecated"] = field_info.deprecated
@ -153,7 +155,11 @@ def get_openapi_operation_request_body(
if required: if required:
request_body_oai["required"] = required request_body_oai["required"] = required
request_media_content: Dict[str, Any] = {"schema": body_schema} 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_media_content["example"] = jsonable_encoder(field_info.example)
request_body_oai["content"] = {request_media_type: request_media_content} request_body_oai["content"] = {request_media_type: request_media_content}
return request_body_oai return request_body_oai

15
fastapi/param_functions.py

@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from fastapi import params from fastapi import params
from fastapi._compat import Undefined from fastapi._compat import Undefined
from fastapi.openapi.models import Example
from typing_extensions import Annotated, deprecated from typing_extensions import Annotated, deprecated
_Unset: Any = Undefined _Unset: Any = Undefined
@ -46,6 +47,7 @@ def Path( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -76,6 +78,7 @@ def Path( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -122,6 +125,7 @@ def Query( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -152,6 +156,7 @@ def Query( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -199,6 +204,7 @@ def Header( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -230,6 +236,7 @@ def Header( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -276,6 +283,7 @@ def Cookie( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -306,6 +314,7 @@ def Cookie( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -354,6 +363,7 @@ def Body( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -386,6 +396,7 @@ def Body( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -433,6 +444,7 @@ def Form( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -464,6 +476,7 @@ def Form( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
@ -511,6 +524,7 @@ def File( # noqa: N802
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -542,6 +556,7 @@ def File( # noqa: N802
decimal_places=decimal_places, decimal_places=decimal_places,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
deprecated=deprecated, deprecated=deprecated,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,

17
fastapi/params.py

@ -2,6 +2,7 @@ import warnings
from enum import Enum from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Union from typing import Any, Callable, Dict, List, Optional, Sequence, Union
from fastapi.openapi.models import Example
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing_extensions import Annotated, deprecated from typing_extensions import Annotated, deprecated
@ -61,6 +62,7 @@ class Param(FieldInfo):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -75,6 +77,7 @@ class Param(FieldInfo):
) )
self.example = example self.example = example
self.include_in_schema = include_in_schema self.include_in_schema = include_in_schema
self.openapi_examples = openapi_examples
kwargs = dict( kwargs = dict(
default=default, default=default,
default_factory=default_factory, default_factory=default_factory,
@ -170,6 +173,7 @@ class Path(Param):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -204,6 +208,7 @@ class Path(Param):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,
@ -254,6 +259,7 @@ class Query(Param):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -286,6 +292,7 @@ class Query(Param):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,
@ -337,6 +344,7 @@ class Header(Param):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -370,6 +378,7 @@ class Header(Param):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,
@ -420,6 +429,7 @@ class Cookie(Param):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -452,6 +462,7 @@ class Cookie(Param):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,
@ -502,6 +513,7 @@ class Body(FieldInfo):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -518,6 +530,7 @@ class Body(FieldInfo):
) )
self.example = example self.example = example
self.include_in_schema = include_in_schema self.include_in_schema = include_in_schema
self.openapi_examples = openapi_examples
kwargs = dict( kwargs = dict(
default=default, default=default,
default_factory=default_factory, default_factory=default_factory,
@ -613,6 +626,7 @@ class Form(Body):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -647,6 +661,7 @@ class Form(Body):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,
@ -696,6 +711,7 @@ class File(Form):
"although still supported. Use examples instead." "although still supported. Use examples instead."
), ),
] = _Unset, ] = _Unset,
openapi_examples: Optional[Dict[str, Example]] = None,
deprecated: Optional[bool] = None, deprecated: Optional[bool] = None,
include_in_schema: bool = True, include_in_schema: bool = True,
json_schema_extra: Union[Dict[str, Any], None] = None, json_schema_extra: Union[Dict[str, Any], None] = None,
@ -729,6 +745,7 @@ class File(Form):
deprecated=deprecated, deprecated=deprecated,
example=example, example=example,
examples=examples, examples=examples,
openapi_examples=openapi_examples,
include_in_schema=include_in_schema, include_in_schema=include_in_schema,
json_schema_extra=json_schema_extra, json_schema_extra=json_schema_extra,
**extra, **extra,

455
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",
},
}
},
}

166
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"},
},
},
}
},
}

166
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"},
},
},
}
},
}

170
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"},
},
},
}
},
}

170
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"},
},
},
}
},
}

170
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"},
},
},
}
},
}
Loading…
Cancel
Save