Repository: litestar-org/litestar

Issue:
Enhancement: Add `ResponseSpec(examples=...)`
### Summary

Support adding custom examples for the response body.

The inbound parameters can be defined via `Parameter`, but the same does not work for the response body. `ResponseSpec` only has `generate_examples`, but it should support also `examples` for custom ones.

### Basic Example

_No response_

### Drawbacks and Impact

_No response_

### Unresolved questions

_No response_

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/3068">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3068/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3068/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Related, any thoughts about supporting `Parameter` (-like) construct for response annotation?

```py
@get()
def endpoint() -> Annotated[ResponseModel, ResponseSpec(examples=[...])]: ...
```
> Related, any thoughts about supporting `Parameter` (-like) construct for response annotation?
> 
> ```python
> @get()
> def endpoint() -> Annotated[ResponseModel, ResponseSpec(examples=[...])]: ...
> ```

This feels reasonable to me.

Resolving patch:
diff --git a/litestar/_openapi/responses.py b/litestar/_openapi/responses.py
--- a/litestar/_openapi/responses.py
+++ b/litestar/_openapi/responses.py
@@ -9,9 +9,10 @@
 from typing import TYPE_CHECKING, Any, Iterator
 
 from litestar._openapi.schema_generation import SchemaCreator
+from litestar._openapi.schema_generation.utils import get_formatted_examples
 from litestar.enums import MediaType
 from litestar.exceptions import HTTPException, ValidationException
-from litestar.openapi.spec import Example, OpenAPIResponse
+from litestar.openapi.spec import Example, OpenAPIResponse, Reference
 from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
 from litestar.openapi.spec.header import OpenAPIHeader
 from litestar.openapi.spec.media_type import OpenAPIMediaType
@@ -240,14 +241,20 @@ def create_additional_responses(self) -> Iterator[tuple[str, OpenAPIResponse]]:
                 prefer_alias=False,
                 generate_examples=additional_response.generate_examples,
             )
-            schema = schema_creator.for_field_definition(
-                FieldDefinition.from_annotation(additional_response.data_container)
-            )
+            field_def = FieldDefinition.from_annotation(additional_response.data_container)
+            schema = schema_creator.for_field_definition(field_def)
+
+            examples: dict[str, Example | Reference] | None
+            if additional_response.examples:
+                examples = dict(get_formatted_examples(field_def, additional_response.examples))
+            else:
+                examples = None
+
             yield (
                 str(status_code),
                 OpenAPIResponse(
                     description=additional_response.description,
-                    content={additional_response.media_type: OpenAPIMediaType(schema=schema)},
+                    content={additional_response.media_type: OpenAPIMediaType(schema=schema, examples=examples)},
                 ),
             )
 
diff --git a/litestar/openapi/datastructures.py b/litestar/openapi/datastructures.py
--- a/litestar/openapi/datastructures.py
+++ b/litestar/openapi/datastructures.py
@@ -6,6 +6,7 @@
 from litestar.enums import MediaType
 
 if TYPE_CHECKING:
+    from litestar.openapi.spec import Example
     from litestar.types import DataContainerType
 
 
@@ -24,3 +25,5 @@ class ResponseSpec:
     """A description of the response."""
     media_type: MediaType = field(default=MediaType.JSON)
     """Response media type."""
+    examples: list[Example] | None = field(default=None)
+    """A list of Example models."""


Test patch:
diff --git a/tests/unit/test_openapi/test_responses.py b/tests/unit/test_openapi/test_responses.py
--- a/tests/unit/test_openapi/test_responses.py
+++ b/tests/unit/test_openapi/test_responses.py
@@ -28,7 +28,7 @@
 from litestar.handlers import HTTPRouteHandler
 from litestar.openapi.config import OpenAPIConfig
 from litestar.openapi.datastructures import ResponseSpec
-from litestar.openapi.spec import OpenAPIHeader, OpenAPIMediaType, Reference, Schema
+from litestar.openapi.spec import Example, OpenAPIHeader, OpenAPIMediaType, Reference, Schema
 from litestar.openapi.spec.enums import OpenAPIType
 from litestar.response import File, Redirect, Stream, Template
 from litestar.response.base import T
@@ -416,6 +416,28 @@ def handler() -> DataclassPerson:
     assert responses["400"].description == "Overwritten response"
 
 
+def test_additional_responses_with_custom_examples(create_factory: CreateFactoryFixture) -> None:
+    @get(responses={200: ResponseSpec(DataclassPerson, examples=[Example(value={"string": "example", "number": 1})])})
+    def handler() -> DataclassPerson:
+        return DataclassPersonFactory.build()
+
+    factory = create_factory(handler)
+    responses = factory.create_additional_responses()
+    status_code, response = next(responses)
+    assert response.content
+    assert response.content["application/json"].examples == {
+        "dataclassperson-example-1": Example(
+            value={
+                "string": "example",
+                "number": 1,
+            }
+        ),
+    }
+
+    with pytest.raises(StopIteration):
+        next(responses)
+
+
 def test_create_response_for_response_subclass(create_factory: CreateFactoryFixture) -> None:
     class CustomResponse(Response[T]):
         pass