diff --git a/litestar/_openapi/request_body.py b/litestar/_openapi/request_body.py index 7a5cf37de5..92a4bde088 100644 --- a/litestar/_openapi/request_body.py +++ b/litestar/_openapi/request_body.py @@ -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 @@ -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 @@ -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)}) diff --git a/litestar/openapi/spec/media_type.py b/litestar/openapi/spec/media_type.py index 3e83fb5e0a..94bd4566dc 100644 --- a/litestar/openapi/spec/media_type.py +++ b/litestar/openapi/spec/media_type.py @@ -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 @@ -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 """Examples of the media type. Each example object SHOULD match the media type and specified schema if present. diff --git a/tests/unit/test_openapi/test_request_body.py b/tests/unit/test_openapi/test_request_body.py index 0badda8ac2..84d84951ac 100644 --- a/tests/unit/test_openapi/test_request_body.py +++ b/tests/unit/test_openapi/test_request_body.py @@ -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 @@ -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}