Repository: litestar-org/litestar

Issue:
Bug: `null` type OpenAPI response generated for ResponseSpec(None) / 204
### Description

Attempting to document (empty) error response description something like:

```py
    responses={
        204: ResponseSpec(None, description="Data successfully wiped")
    }
```

`None` is the required `data_container` argument.

This leads to:

```json
        "responses": {
          "204": {
            "description": "Data successfully wiped",
            "content": {
              "application/json": {
                "schema": {
                  "type": "null",
                  "examples": {
                    "nonetype-example-1": {
                      "description": "Example  value"
                    }
                  }
                }
              }
            }
          }
```

It:

- Generates `content`, which should rather be missing
- Generates example withing the `content`, even though `create_examples` was not enabled (false by default). This is likely related with https://github.com/litestar-org/litestar/issues/3058#issuecomment-1924015790

The `content` should not be there. https://swagger.io/docs/specification/describing-responses/:

> **Response Body**
> Some responses, such as `204 No Content`, have no body. To indicate the response body is empty, do not specify a content for the response:
> 
>       responses:
>         '204':
>           description: The resource was deleted successfully.

If `None` is incorrect value to pass here, what would be the right one, to adjust the 204 response?

### MCVE

```python
import uvicorn
from litestar import Litestar, delete
from litestar.openapi import ResponseSpec


@delete(
    responses={
        204: ResponseSpec(None, description="Data successfully deleted")
    }
)
async def delete_user_data() -> None:
    pass


app = Litestar(
    route_handlers=[delete_user_data],
)

uvicorn.run(app)
```


### Steps to reproduce

```bash
curl http://localhost:8000/schema/openapi.json
```

```json
{
  "info": {
    "title": "Litestar API",
    "version": "1.0.0"
  },
  "openapi": "3.1.0",
  "servers": [
    {
      "url": "/"
    }
  ],
  "paths": {
    "/": {
      "delete": {
        "summary": "DeleteUserData",
        "operationId": "DeleteUserData",
        "responses": {
          "204": {
            "description": "Data successfully deleted",
            "content": {  # <--------- this shouldn't be here
              "application/json": {
                "schema": {
                  "type": "null",
                  "examples": {
                    "nonetype-example-1": {
                      "description": "Example  value"
                    }
                  }
                }
              }
            }
          }
        },
        "deprecated": false
      }
    }
  },
  "components": {
    "schemas": {}
  }
}
```

### Litestar Version

2.5.1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- 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/3069">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3069/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3069/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Seems like a reasonable assessment. Would you be interested in submitting a fix for this?
Yeah, I can try have a look in the coming week.

Resolving patch:
diff --git a/litestar/_openapi/responses.py b/litestar/_openapi/responses.py
--- a/litestar/_openapi/responses.py
+++ b/litestar/_openapi/responses.py
@@ -240,14 +240,21 @@ 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)
-            )
+
+            content: dict[str, OpenAPIMediaType] | None
+            if additional_response.data_container is not None:
+                schema = schema_creator.for_field_definition(
+                    FieldDefinition.from_annotation(additional_response.data_container)
+                )
+                content = {additional_response.media_type: OpenAPIMediaType(schema=schema)}
+            else:
+                content = None
+
             yield (
                 str(status_code),
                 OpenAPIResponse(
                     description=additional_response.description,
-                    content={additional_response.media_type: OpenAPIMediaType(schema=schema)},
+                    content=content,
                 ),
             )
 
diff --git a/litestar/openapi/datastructures.py b/litestar/openapi/datastructures.py
--- a/litestar/openapi/datastructures.py
+++ b/litestar/openapi/datastructures.py
@@ -16,7 +16,7 @@
 class ResponseSpec:
     """Container type of additional responses."""
 
-    data_container: DataContainerType
+    data_container: DataContainerType | None
     """A model that describes the content of the response."""
     generate_examples: bool = field(default=True)
     """Generate examples for the response content."""


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
@@ -11,7 +11,7 @@
 import pytest
 from typing_extensions import TypeAlias
 
-from litestar import Controller, Litestar, MediaType, Response, get, post
+from litestar import Controller, Litestar, MediaType, Response, delete, get, post
 from litestar._openapi.datastructures import OpenAPIContext
 from litestar._openapi.responses import (
     ResponseFactory,
@@ -35,6 +35,7 @@
 from litestar.routes import HTTPRoute
 from litestar.status_codes import (
     HTTP_200_OK,
+    HTTP_204_NO_CONTENT,
     HTTP_307_TEMPORARY_REDIRECT,
     HTTP_400_BAD_REQUEST,
     HTTP_406_NOT_ACCEPTABLE,
@@ -282,6 +283,29 @@ def redirect_handler() -> Redirect:
     assert location.description
 
 
+def test_create_success_response_no_content_explicit_responsespec(
+    create_factory: CreateFactoryFixture,
+) -> None:
+    @delete(
+        path="/test",
+        responses={HTTP_204_NO_CONTENT: ResponseSpec(None, description="Custom description")},
+        name="test",
+    )
+    def handler() -> None:
+        return None
+
+    handler = get_registered_route_handler(handler, "test")
+    factory = create_factory(handler)
+    responses = factory.create_additional_responses()
+    status, response = next(responses)
+    assert status == "204"
+    assert response.description == "Custom description"
+    assert not response.content
+
+    with pytest.raises(StopIteration):
+        next(responses)
+
+
 def test_create_success_response_file_data(create_factory: CreateFactoryFixture) -> None:
     @get(path="/test", name="test")
     def file_handler() -> File:
diff --git a/tests/unit/test_openapi/test_spec_generation.py b/tests/unit/test_openapi/test_spec_generation.py
--- a/tests/unit/test_openapi/test_spec_generation.py
+++ b/tests/unit/test_openapi/test_spec_generation.py
@@ -5,7 +5,10 @@
 import pytest
 from msgspec import Struct
 
-from litestar import post
+from litestar import delete, post
+from litestar.openapi import ResponseSpec
+from litestar.openapi.spec import OpenAPI
+from litestar.status_codes import HTTP_204_NO_CONTENT
 from litestar.testing import create_test_client
 from tests.models import DataclassPerson, MsgSpecStructPerson, TypedDictPerson
 
@@ -48,6 +51,33 @@ def handler(data: cls) -> cls:
         }
 
 
+def test_spec_generation_no_content() -> None:
+    @delete(
+        "/",
+        status_code=HTTP_204_NO_CONTENT,
+        responses={204: ResponseSpec(None, description="Custom response")},
+    )
+    def handler() -> None:
+        return None
+
+    with create_test_client(handler) as client:
+        schema: OpenAPI = client.app.openapi_schema
+        assert schema.to_schema()["paths"] == {
+            "/": {
+                "delete": {
+                    "summary": "Handler",
+                    "deprecated": False,
+                    "operationId": "Handler",
+                    "responses": {
+                        "204": {
+                            "description": "Custom response",
+                        }
+                    },
+                },
+            },
+        }
+
+
 def test_msgspec_schema() -> None:
     class CamelizedStruct(Struct, rename="camel"):
         field_one: int