Skip to content

Commit

Permalink
✨ Add support for adding multiple examples in request bodies and path…
Browse files Browse the repository at this point in the history
…, query, cookie, and header params (#1267)

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
  • Loading branch information
austinorr and tiangolo committed May 5, 2021
1 parent 3e32eb5 commit e10a437
Show file tree
Hide file tree
Showing 9 changed files with 1,220 additions and 24 deletions.
Binary file added docs/en/docs/img/tutorial/body-fields/image02.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 70 additions & 19 deletions docs/en/docs/tutorial/schema-extra-example.md
@@ -1,58 +1,109 @@
# Schema Extra - Example
# Declare Request Example Data

You can define extra information to go in JSON Schema.
You can declare examples of the data your app can receive.

A common use case is to add an `example` that will be shown in the docs.

There are several ways you can declare extra JSON Schema information.
Here are several ways to do it.

## Pydantic `schema_extra`

You can declare an example for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:
You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:

```Python hl_lines="15-23"
{!../../../docs_src/schema_extra_example/tutorial001.py!}
```

That extra info will be added as-is to the output JSON Schema.
That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.

!!! tip
You could use the same technique to extend the JSON Schema and add your own custom extra info.

For example you could use it to add metadata for a frontend user interface, etc.

## `Field` additional arguments

In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can also declare extra info for the JSON Schema by passing any other arbitrary arguments to the function, for example, to add an `example`:
When using `Field()` with Pydantic models, you can also declare extra info for the **JSON Schema** by passing any other arbitrary arguments to the function.

You can use this to add `example` for each field:

```Python hl_lines="4 10-13"
{!../../../docs_src/schema_extra_example/tutorial002.py!}
```

!!! warning
Keep in mind that those extra arguments passed won't add any validation, only annotation, for documentation purposes.
Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes.

## `example` and `examples` in OpenAPI

When using any of:

## `Body` additional arguments
* `Path()`
* `Query()`
* `Header()`
* `Cookie()`
* `Body()`
* `Form()`
* `File()`

The same way you can pass extra info to `Field`, you can do the same with `Path`, `Query`, `Body`, etc.
you can also declare a data `example` or a group of `examples` with additional information that will be added to **OpenAPI**.

For example, you can pass an `example` for a body request to `Body`:
### `Body` with `example`

Here we pass an `example` of the data expected in `Body()`:

```Python hl_lines="21-26"
{!../../../docs_src/schema_extra_example/tutorial003.py!}
```

## Example in the docs UI
### Example in the docs UI

With any of the methods above it would look like this in the `/docs`:

<img src="/img/tutorial/body-fields/image01.png">

### `Body` with multiple `examples`

Alternatively to the single `example`, you can pass `examples` using 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`.

```Python hl_lines="22-48"
{!../../../docs_src/schema_extra_example/tutorial004.py!}
```

### Examples in the docs UI

With `examples` added to `Body()` the `/docs` would look like:

<img src="/img/tutorial/body-fields/image02.png">

## Technical Details

About `example` vs `examples`...
!!! warning
These are very technical details about the standards **JSON Schema** and **OpenAPI**.

If the ideas above already work for you, that might me enough, and you probably don't need these details, feel free to skip them.

When you add an example inside of a Pydantic model, using `schema_extra` or `Field(example="something")` that example is added to the **JSON Schema** for that Pydantic model.

And that **JSON Schema** of the Pydantic model is included in the **OpenAPI** of your API, and then it's used in the docs UI.

**JSON Schema** doesn't really have a field `example` in the standards. Recent versions of JSON Schema define a field <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>, but OpenAPI 3.0.3 is based on an older version of JSON Schema that didn't have `examples`.

So, OpenAPI 3.0.3 defined its own <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> for the modified version of **JSON Schema** it uses, for the same purpose (but it's a single `example`, not `examples`), and that's what is used by the API docs UI (using Swagger UI).

JSON Schema defines a field <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a> in the most recent versions, but OpenAPI is based on an older version of JSON Schema that didn't have `examples`.
So, although `example` is not part of JSON Schema, it is part of OpenAPI's custom version of JSON Schema, and that's what will be used by the docs UI.

So, OpenAPI defined its own <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> for the same purpose (as `example`, not `examples`), and that's what is used by the docs UI (using Swagger UI).
But when you use `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples are not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they are added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema).

So, although `example` is not part of JSON Schema, it is part of OpenAPI, and that's what will be used by the docs UI.
For `Path()`, `Query()`, `Header()`, and `Cookie()`, the `example` or `examples` are added to the <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object" class="external-link" target="_blank">OpenAPI definition, to the `Parameter Object` (in the specification)</a>.

## Other info
And for `Body()`, `File()`, and `Form()`, the `example` or `examples` are equivalently added to the <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject" class="external-link" target="_blank">OpenAPI definition, to the `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification)</a>.

The same way, you could add your own custom extra info that would be added to the JSON Schema for each model, for example to customize a frontend user interface, etc.
On the other hand, there's a newer version of OpenAPI: **3.1.0**, recently released. It is based on the latest JSON Schema and most of the modifications from OpenAPI's custom version of JSON Schema are removed, in exchange of the features from the recent versions of JSON Schema, so all these small differences are reduced. Nevertheless, Swagger UI currently doesn't support OpenAPI 3.1.0, so, for now, it's better to continue using the ideas above.
52 changes: 52 additions & 0 deletions docs_src/schema_extra_example/tutorial004.py
@@ -0,0 +1,52 @@
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None


@app.put("/items/{item_id}")
async def update_item(
*,
item_id: int,
item: Item = Body(
...,
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
13 changes: 11 additions & 2 deletions fastapi/openapi/utils.py
Expand Up @@ -21,7 +21,7 @@
get_model_definitions,
)
from pydantic import BaseModel
from pydantic.fields import ModelField
from pydantic.fields import ModelField, Undefined
from pydantic.schema import (
field_schema,
get_flat_models_from_fields,
Expand Down Expand Up @@ -101,6 +101,10 @@ def get_openapi_operation_parameters(
}
if field_info.description:
parameter["description"] = field_info.description
if field_info.examples:
parameter["examples"] = jsonable_encoder(field_info.examples)
elif field_info.example != Undefined:
parameter["example"] = jsonable_encoder(field_info.example)
if field_info.deprecated:
parameter["deprecated"] = field_info.deprecated
parameters.append(parameter)
Expand All @@ -124,7 +128,12 @@ def get_openapi_operation_request_body(
request_body_oai: Dict[str, Any] = {}
if required:
request_body_oai["required"] = required
request_body_oai["content"] = {request_media_type: {"schema": body_schema}}
request_media_content: Dict[str, Any] = {"schema": body_schema}
if field_info.examples:
request_media_content["examples"] = jsonable_encoder(field_info.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


Expand Down
31 changes: 30 additions & 1 deletion fastapi/param_functions.py
@@ -1,6 +1,7 @@
from typing import Any, Callable, Optional, Sequence
from typing import Any, Callable, Dict, Optional, Sequence

from fastapi import params
from pydantic.fields import Undefined


def Path( # noqa: N802
Expand All @@ -16,6 +17,8 @@ def Path( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
**extra: Any,
) -> Any:
Expand All @@ -31,6 +34,8 @@ def Path( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
deprecated=deprecated,
**extra,
)
Expand All @@ -49,6 +54,8 @@ def Query( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
**extra: Any,
) -> Any:
Expand All @@ -64,6 +71,8 @@ def Query( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
deprecated=deprecated,
**extra,
)
Expand All @@ -83,6 +92,8 @@ def Header( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
**extra: Any,
) -> Any:
Expand All @@ -99,6 +110,8 @@ def Header( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
deprecated=deprecated,
**extra,
)
Expand All @@ -117,6 +130,8 @@ def Cookie( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
deprecated: Optional[bool] = None,
**extra: Any,
) -> Any:
Expand All @@ -132,6 +147,8 @@ def Cookie( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
deprecated=deprecated,
**extra,
)
Expand All @@ -152,6 +169,8 @@ def Body( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
**extra: Any,
) -> Any:
return params.Body(
Expand All @@ -168,6 +187,8 @@ def Body( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
**extra,
)

Expand All @@ -186,6 +207,8 @@ def Form( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
**extra: Any,
) -> Any:
return params.Form(
Expand All @@ -201,6 +224,8 @@ def Form( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
**extra,
)

Expand All @@ -219,6 +244,8 @@ def File( # noqa: N802
min_length: Optional[int] = None,
max_length: Optional[int] = None,
regex: Optional[str] = None,
example: Any = Undefined,
examples: Optional[Dict[str, Any]] = None,
**extra: Any,
) -> Any:
return params.File(
Expand All @@ -234,6 +261,8 @@ def File( # noqa: N802
min_length=min_length,
max_length=max_length,
regex=regex,
example=example,
examples=examples,
**extra,
)

Expand Down

0 comments on commit e10a437

Please sign in to comment.