]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add support for adding multiple examples in request bodies and path, query, cookie...
authorAustin Orr <austinorr@users.noreply.github.com>
Wed, 5 May 2021 18:20:56 +0000 (11:20 -0700)
committerGitHub <noreply@github.com>
Wed, 5 May 2021 18:20:56 +0000 (20:20 +0200)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
docs/en/docs/img/tutorial/body-fields/image02.png [new file with mode: 0644]
docs/en/docs/tutorial/schema-extra-example.md
docs_src/schema_extra_example/tutorial004.py [new file with mode: 0644]
fastapi/openapi/utils.py
fastapi/param_functions.py
fastapi/params.py
tests/test_schema_extra_examples.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial004.py [new file with mode: 0644]

diff --git a/docs/en/docs/img/tutorial/body-fields/image02.png b/docs/en/docs/img/tutorial/body-fields/image02.png
new file mode 100644 (file)
index 0000000..f307bb8
Binary files /dev/null and b/docs/en/docs/img/tutorial/body-fields/image02.png differ
index 11c89c084d4c8cd6f7bcfe380dad5e77a95ee06c..47ee8503a9d684e4d25ddd95c3af64f3d3e89b5d 100644 (file)
-# 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.
diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py
new file mode 100644 (file)
index 0000000..9f0e8b4
--- /dev/null
@@ -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
index 410ba9389c5b673f596127f4a2730733d7864e1a..6f749ef9c264a7a48b03ba0d0ab084f84412cce4 100644 (file)
@@ -21,7 +21,7 @@ from fastapi.utils import (
     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,
@@ -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)
@@ -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
 
 
index 9ebb59100081ed2332c096a2e143bdad937369ca..ff65d7271281b423a47772bb9d8bffaf5b4bb3a9 100644 (file)
@@ -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
@@ -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:
@@ -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,
     )
@@ -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:
@@ -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,
     )
@@ -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:
@@ -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,
     )
@@ -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:
@@ -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,
     )
@@ -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(
@@ -168,6 +187,8 @@ def Body(  # noqa: N802
         min_length=min_length,
         max_length=max_length,
         regex=regex,
+        example=example,
+        examples=examples,
         **extra,
     )
 
@@ -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(
@@ -201,6 +224,8 @@ def Form(  # noqa: N802
         min_length=min_length,
         max_length=max_length,
         regex=regex,
+        example=example,
+        examples=examples,
         **extra,
     )
 
@@ -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(
@@ -234,6 +261,8 @@ def File(  # noqa: N802
         min_length=min_length,
         max_length=max_length,
         regex=regex,
+        example=example,
+        examples=examples,
         **extra,
     )
 
index aa3269a8054c1e85656204a36cbc6fcf79389e80..3cab98b78dff15d7c32d47d1f5646f40149c82f9 100644 (file)
@@ -1,7 +1,7 @@
 from enum import Enum
-from typing import Any, Callable, Optional, Sequence
+from typing import Any, Callable, Dict, Optional, Sequence
 
-from pydantic.fields import FieldInfo
+from pydantic.fields import FieldInfo, Undefined
 
 
 class ParamTypes(Enum):
@@ -28,10 +28,14 @@ class Param(FieldInfo):
         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,
     ):
         self.deprecated = deprecated
+        self.example = example
+        self.examples = examples
         super().__init__(
             default,
             alias=alias,
@@ -68,6 +72,8 @@ class Path(Param):
         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,
     ):
@@ -85,6 +91,8 @@ class Path(Param):
             max_length=max_length,
             regex=regex,
             deprecated=deprecated,
+            example=example,
+            examples=examples,
             **extra,
         )
 
@@ -106,6 +114,8 @@ class Query(Param):
         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,
     ):
@@ -122,6 +132,8 @@ class Query(Param):
             max_length=max_length,
             regex=regex,
             deprecated=deprecated,
+            example=example,
+            examples=examples,
             **extra,
         )
 
@@ -144,6 +156,8 @@ class Header(Param):
         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,
     ):
@@ -161,6 +175,8 @@ class Header(Param):
             max_length=max_length,
             regex=regex,
             deprecated=deprecated,
+            example=example,
+            examples=examples,
             **extra,
         )
 
@@ -182,6 +198,8 @@ class Cookie(Param):
         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,
     ):
@@ -198,6 +216,8 @@ class Cookie(Param):
             max_length=max_length,
             regex=regex,
             deprecated=deprecated,
+            example=example,
+            examples=examples,
             **extra,
         )
 
@@ -219,10 +239,14 @@ class Body(FieldInfo):
         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,
     ):
         self.embed = embed
         self.media_type = media_type
+        self.example = example
+        self.examples = examples
         super().__init__(
             default,
             alias=alias,
@@ -258,6 +282,8 @@ class Form(Body):
         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,
     ):
         super().__init__(
@@ -274,6 +300,8 @@ class Form(Body):
             min_length=min_length,
             max_length=max_length,
             regex=regex,
+            example=example,
+            examples=examples,
             **extra,
         )
 
@@ -294,6 +322,8 @@ class File(Form):
         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,
     ):
         super().__init__(
@@ -309,6 +339,8 @@ class File(Form):
             min_length=min_length,
             max_length=max_length,
             regex=regex,
+            example=example,
+            examples=examples,
             **extra,
         )
 
diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py
new file mode 100644 (file)
index 0000000..3e0d846
--- /dev/null
@@ -0,0 +1,889 @@
+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
+
+    class Config:
+        schema_extra = {"example": {"data": "Data in schema_extra"}}
+
+
+@app.post("/schema_extra/")
+def schema_extra(item: Item):
+    return item
+
+
+@app.post("/example/")
+def example(item: Item = Body(..., example={"data": "Data in Body example"})):
+    return item
+
+
+@app.post("/examples/")
+def examples(
+    item: Item = Body(
+        ...,
+        examples={
+            "example1": {
+                "summary": "example1 summary",
+                "value": {"data": "Data in Body examples, example1"},
+            },
+            "example2": {"value": {"data": "Data in Body examples, example2"}},
+        },
+    )
+):
+    return item
+
+
+@app.post("/example_examples/")
+def example_examples(
+    item: Item = Body(
+        ...,
+        example={"data": "Overriden example"},
+        examples={
+            "example1": {"value": {"data": "examples example_examples 1"}},
+            "example2": {"value": {"data": "examples example_examples 2"}},
+        },
+    )
+):
+    return item
+
+
+# TODO: enable these tests once/if Form(embed=False) is supported
+# TODO: In that case, define if File() should support example/examples too
+# @app.post("/form_example")
+# def form_example(firstname: str = Form(..., example="John")):
+#     return firstname
+
+
+# @app.post("/form_examples")
+# def form_examples(
+#     lastname: str = Form(
+#         ...,
+#         examples={
+#             "example1": {"summary": "last name summary", "value": "Doe"},
+#             "example2": {"value": "Doesn't"},
+#         },
+#     ),
+# ):
+#     return lastname
+
+
+# @app.post("/form_example_examples")
+# def form_example_examples(
+#     lastname: str = Form(
+#         ...,
+#         example="Doe overriden",
+#         examples={
+#             "example1": {"summary": "last name summary", "value": "Doe"},
+#             "example2": {"value": "Doesn't"},
+#         },
+#     ),
+# ):
+#     return lastname
+
+
+@app.get("/path_example/{item_id}")
+def path_example(
+    item_id: str = Path(
+        ...,
+        example="item_1",
+    ),
+):
+    return item_id
+
+
+@app.get("/path_examples/{item_id}")
+def path_examples(
+    item_id: str = Path(
+        ...,
+        examples={
+            "example1": {"summary": "item ID summary", "value": "item_1"},
+            "example2": {"value": "item_2"},
+        },
+    ),
+):
+    return item_id
+
+
+@app.get("/path_example_examples/{item_id}")
+def path_example_examples(
+    item_id: str = Path(
+        ...,
+        example="item_overriden",
+        examples={
+            "example1": {"summary": "item ID summary", "value": "item_1"},
+            "example2": {"value": "item_2"},
+        },
+    ),
+):
+    return item_id
+
+
+@app.get("/query_example/")
+def query_example(
+    data: str = Query(
+        None,
+        example="query1",
+    ),
+):
+    return data
+
+
+@app.get("/query_examples/")
+def query_examples(
+    data: str = Query(
+        None,
+        examples={
+            "example1": {"summary": "Query example 1", "value": "query1"},
+            "example2": {"value": "query2"},
+        },
+    ),
+):
+    return data
+
+
+@app.get("/query_example_examples/")
+def query_example_examples(
+    data: str = Query(
+        None,
+        example="query_overriden",
+        examples={
+            "example1": {"summary": "Qeury example 1", "value": "query1"},
+            "example2": {"value": "query2"},
+        },
+    ),
+):
+    return data
+
+
+@app.get("/header_example/")
+def header_example(
+    data: str = Header(
+        None,
+        example="header1",
+    ),
+):
+    return data
+
+
+@app.get("/header_examples/")
+def header_examples(
+    data: str = Header(
+        None,
+        examples={
+            "example1": {"summary": "header example 1", "value": "header1"},
+            "example2": {"value": "header2"},
+        },
+    ),
+):
+    return data
+
+
+@app.get("/header_example_examples/")
+def header_example_examples(
+    data: str = Header(
+        None,
+        example="header_overriden",
+        examples={
+            "example1": {"summary": "Qeury example 1", "value": "header1"},
+            "example2": {"value": "header2"},
+        },
+    ),
+):
+    return data
+
+
+@app.get("/cookie_example/")
+def cookie_example(
+    data: str = Cookie(
+        None,
+        example="cookie1",
+    ),
+):
+    return data
+
+
+@app.get("/cookie_examples/")
+def cookie_examples(
+    data: str = Cookie(
+        None,
+        examples={
+            "example1": {"summary": "cookie example 1", "value": "cookie1"},
+            "example2": {"value": "cookie2"},
+        },
+    ),
+):
+    return data
+
+
+@app.get("/cookie_example_examples/")
+def cookie_example_examples(
+    data: str = Cookie(
+        None,
+        example="cookie_overriden",
+        examples={
+            "example1": {"summary": "Qeury example 1", "value": "cookie1"},
+            "example2": {"value": "cookie2"},
+        },
+    ),
+):
+    return data
+
+
+client = TestClient(app)
+
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "FastAPI", "version": "0.1.0"},
+    "paths": {
+        "/schema_extra/": {
+            "post": {
+                "summary": "Schema Extra",
+                "operationId": "schema_extra_schema_extra__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/Item"}
+                        }
+                    },
+                    "required": True,
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/example/": {
+            "post": {
+                "summary": "Example",
+                "operationId": "example_example__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/Item"},
+                            "example": {"data": "Data in Body example"},
+                        }
+                    },
+                    "required": True,
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/examples/": {
+            "post": {
+                "summary": "Examples",
+                "operationId": "examples_examples__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/Item"},
+                            "examples": {
+                                "example1": {
+                                    "summary": "example1 summary",
+                                    "value": {
+                                        "data": "Data in Body examples, example1"
+                                    },
+                                },
+                                "example2": {
+                                    "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"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/example_examples/": {
+            "post": {
+                "summary": "Example Examples",
+                "operationId": "example_examples_example_examples__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/Item"},
+                            "examples": {
+                                "example1": {
+                                    "value": {"data": "examples example_examples 1"}
+                                },
+                                "example2": {
+                                    "value": {"data": "examples example_examples 2"}
+                                },
+                            },
+                        }
+                    },
+                    "required": True,
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/path_example/{item_id}": {
+            "get": {
+                "summary": "Path Example",
+                "operationId": "path_example_path_example__item_id__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item Id", "type": "string"},
+                        "example": "item_1",
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+                "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": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item Id", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "item ID summary",
+                                "value": "item_1",
+                            },
+                            "example2": {"value": "item_2"},
+                        },
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/path_example_examples/{item_id}": {
+            "get": {
+                "summary": "Path Example Examples",
+                "operationId": "path_example_examples_path_example_examples__item_id__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item Id", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "item ID summary",
+                                "value": "item_1",
+                            },
+                            "example2": {"value": "item_2"},
+                        },
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/query_example/": {
+            "get": {
+                "summary": "Query Example",
+                "operationId": "query_example_query_example__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "example": "query1",
+                        "name": "data",
+                        "in": "query",
+                    }
+                ],
+                "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": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "Query example 1",
+                                "value": "query1",
+                            },
+                            "example2": {"value": "query2"},
+                        },
+                        "name": "data",
+                        "in": "query",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/query_example_examples/": {
+            "get": {
+                "summary": "Query Example Examples",
+                "operationId": "query_example_examples_query_example_examples__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "Qeury example 1",
+                                "value": "query1",
+                            },
+                            "example2": {"value": "query2"},
+                        },
+                        "name": "data",
+                        "in": "query",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/header_example/": {
+            "get": {
+                "summary": "Header Example",
+                "operationId": "header_example_header_example__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "example": "header1",
+                        "name": "data",
+                        "in": "header",
+                    }
+                ],
+                "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": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "header example 1",
+                                "value": "header1",
+                            },
+                            "example2": {"value": "header2"},
+                        },
+                        "name": "data",
+                        "in": "header",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/header_example_examples/": {
+            "get": {
+                "summary": "Header Example Examples",
+                "operationId": "header_example_examples_header_example_examples__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "Qeury example 1",
+                                "value": "header1",
+                            },
+                            "example2": {"value": "header2"},
+                        },
+                        "name": "data",
+                        "in": "header",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/cookie_example/": {
+            "get": {
+                "summary": "Cookie Example",
+                "operationId": "cookie_example_cookie_example__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "example": "cookie1",
+                        "name": "data",
+                        "in": "cookie",
+                    }
+                ],
+                "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": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "cookie example 1",
+                                "value": "cookie1",
+                            },
+                            "example2": {"value": "cookie2"},
+                        },
+                        "name": "data",
+                        "in": "cookie",
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/cookie_example_examples/": {
+            "get": {
+                "summary": "Cookie Example Examples",
+                "operationId": "cookie_example_examples_cookie_example_examples__get",
+                "parameters": [
+                    {
+                        "required": False,
+                        "schema": {"title": "Data", "type": "string"},
+                        "examples": {
+                            "example1": {
+                                "summary": "Qeury example 1",
+                                "value": "cookie1",
+                            },
+                            "example2": {"value": "cookie2"},
+                        },
+                        "name": "data",
+                        "in": "cookie",
+                    }
+                ],
+                "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": ["data"],
+                "type": "object",
+                "properties": {"data": {"title": "Data", "type": "string"}},
+                "example": {"data": "Data in schema_extra"},
+            },
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    """
+    Test that example overrides work:
+
+    * pydantic model schema_extra is included
+    * Body(example={}) overrides schema_extra in pydantic model
+    * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model
+    """
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == openapi_schema
+
+
+def test_call_api():
+    response = client.post("/schema_extra/", json={"data": "Foo"})
+    assert response.status_code == 200, response.text
+    response = client.post("/example/", json={"data": "Foo"})
+    assert response.status_code == 200, response.text
+    response = client.post("/examples/", json={"data": "Foo"})
+    assert response.status_code == 200, response.text
+    response = client.post("/example_examples/", json={"data": "Foo"})
+    assert response.status_code == 200, response.text
+    response = client.get("/path_example/foo")
+    assert response.status_code == 200, response.text
+    response = client.get("/path_examples/foo")
+    assert response.status_code == 200, response.text
+    response = client.get("/path_example_examples/foo")
+    assert response.status_code == 200, response.text
+    response = client.get("/query_example/")
+    assert response.status_code == 200, response.text
+    response = client.get("/query_examples/")
+    assert response.status_code == 200, response.text
+    response = client.get("/query_example_examples/")
+    assert response.status_code == 200, response.text
+    response = client.get("/header_example/")
+    assert response.status_code == 200, response.text
+    response = client.get("/header_examples/")
+    assert response.status_code == 200, response.text
+    response = client.get("/header_example_examples/")
+    assert response.status_code == 200, response.text
+    response = client.get("/cookie_example/")
+    assert response.status_code == 200, response.text
+    response = client.get("/cookie_examples/")
+    assert response.status_code == 200, response.text
+    response = client.get("/cookie_example_examples/")
+    assert response.status_code == 200, response.text
diff --git a/tests/test_tutorial/test_schema_extra_example/__init__.py b/tests/test_tutorial/test_schema_extra_example/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py
new file mode 100644 (file)
index 0000000..89f5b66
--- /dev/null
@@ -0,0 +1,134 @@
+from fastapi.testclient import TestClient
+
+from docs_src.schema_extra_example.tutorial004 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "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": {"$ref": "#/components/schemas/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": {"title": "Description", "type": "string"},
+                    "price": {"title": "Price", "type": "number"},
+                    "tax": {"title": "Tax", "type": "number"},
+                },
+            },
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == openapi_schema
+
+
+# Test required and embedded body parameters with no bodies sent
+def test_post_body_example():
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200