Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion litestar/_openapi/request_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING

from litestar._openapi.schema_generation import SchemaCreator
from litestar._openapi.schema_generation.utils import get_formatted_examples
from litestar.enums import RequestEncodingType
from litestar.openapi.spec.media_type import OpenAPIMediaType
from litestar.openapi.spec.request_body import RequestBody
Expand All @@ -12,8 +13,11 @@


if TYPE_CHECKING:
from collections.abc import Mapping

from litestar._openapi.datastructures import OpenAPIContext
from litestar.dto import AbstractDTO
from litestar.openapi.spec import Example
from litestar.typing import FieldDefinition


Expand Down Expand Up @@ -48,4 +52,8 @@ def create_request_body(
else:
schema = schema_creator.for_field_definition(data_field)

return RequestBody(required=True, content={media_type: OpenAPIMediaType(schema=schema)})
examples: Mapping[str, Example] | None = None
if isinstance(data_field.kwarg_definition, BodyKwarg) and data_field.kwarg_definition.examples:
examples = dict(get_formatted_examples(data_field, data_field.kwarg_definition.examples)) or None

return RequestBody(required=True, content={media_type: OpenAPIMediaType(schema=schema, examples=examples)})
4 changes: 3 additions & 1 deletion litestar/openapi/spec/media_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from litestar.openapi.spec.base import BaseSchemaObject

if TYPE_CHECKING:
from collections.abc import Mapping

from litestar.openapi.spec.encoding import Encoding
from litestar.openapi.spec.example import Example
from litestar.openapi.spec.reference import Reference
Expand All @@ -32,7 +34,7 @@ class OpenAPIMediaType(BaseSchemaObject):
example provided by the schema.
"""

examples: dict[str, Example | Reference] | None = None
examples: Mapping[str, Example | Reference] | None = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change technically, if you disallow .update in users' typing. It is not a big deal, but I would keep this annotation unchaged if possible :)

"""Examples of the media type.

Each example object SHOULD match the media type and specified schema if present.
Expand Down
147 changes: 146 additions & 1 deletion tests/unit/test_openapi/test_request_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from litestar.enums import RequestEncodingType
from litestar.handlers import BaseRouteHandler
from litestar.openapi.config import OpenAPIConfig
from litestar.openapi.spec import RequestBody
from litestar.openapi.spec import Example, RequestBody
from litestar.params import Body
from litestar.typing import FieldDefinition

Expand Down Expand Up @@ -217,3 +217,148 @@ async def handler(
}
}
}


def test_request_body_examples_in_openapi_schema() -> None:
@post("/items")
async def create_item(
data: Annotated[
dict[str, Any],
Body(
examples=[
Example(summary="Example A", value={"name": "Widget"}),
Example(id="custom-id", summary="Example B", value={"name": "Gadget"}),
]
),
],
) -> dict[str, Any]:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

examples = schema["paths"]["/items"]["post"]["requestBody"]["content"]["application/json"]["examples"]
assert "data-example-1" in examples
assert "custom-id" in examples
assert examples["data-example-1"]["summary"] == "Example A"
assert examples["custom-id"]["value"] == {"name": "Gadget"}


def test_request_body_examples_absent_when_not_set() -> None:
@post("/items")
async def create_item(data: dict[str, Any]) -> dict[str, Any]:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

content = schema["paths"]["/items"]["post"]["requestBody"]["content"]["application/json"]
assert "examples" not in content


def test_request_body_example_with_description() -> None:
@post("/items")
async def create_item(
data: Annotated[
dict[str, Any],
Body(
examples=[
Example(
summary="Full example",
description="A detailed description of this example in **CommonMark**.",
value={"name": "Widget", "price": 9.99},
),
]
),
],
) -> dict[str, Any]:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

examples = schema["paths"]["/items"]["post"]["requestBody"]["content"]["application/json"]["examples"]
example = examples["data-example-1"]
assert example["summary"] == "Full example"
assert example["description"] == "A detailed description of this example in **CommonMark**."
assert example["value"] == {"name": "Widget", "price": 9.99}


def test_request_body_example_with_external_value() -> None:
@post("/items")
async def create_item(
data: Annotated[
dict[str, Any],
Body(
examples=[
Example(
summary="External example",
external_value="https://example.com/samples/item.json",
),
]
),
],
) -> dict[str, Any]:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

examples = schema["paths"]["/items"]["post"]["requestBody"]["content"]["application/json"]["examples"]
example = examples["data-example-1"]
assert example["summary"] == "External example"
assert example["externalValue"] == "https://example.com/samples/item.json"
assert "value" not in example


def test_request_body_examples_with_non_json_media_type() -> None:
@post("/items")
async def create_item(
data: Annotated[
dict[str, Any],
Body(
media_type=RequestEncodingType.URL_ENCODED,
examples=[
Example(summary="Form example", value={"field": "value"}),
],
),
],
) -> dict[str, Any]:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

content = schema["paths"]["/items"]["post"]["requestBody"]["content"]
assert "application/x-www-form-urlencoded" in content
examples = content["application/x-www-form-urlencoded"]["examples"]
assert "data-example-1" in examples
assert examples["data-example-1"]["summary"] == "Form example"


def test_request_body_examples_with_dataclass_model() -> None:
@dataclass
class Item:
name: str
price: float

@post("/items")
async def create_item(
data: Annotated[
Item,
Body(
examples=[
Example(summary="Item example", value={"name": "Widget", "price": 9.99}),
]
),
],
) -> Item:
return data

app = Litestar([create_item])
schema = app.openapi_schema.to_schema()

examples = schema["paths"]["/items"]["post"]["requestBody"]["content"]["application/json"]["examples"]
assert "data-example-1" in examples
assert examples["data-example-1"]["summary"] == "Item example"
assert examples["data-example-1"]["value"] == {"name": "Widget", "price": 9.99}
Loading