Repository: litestar-org/litestar

Issue:
Bug: `default` not generated in OpenAPI schema for body as models
### Description

Declaring default fields (various ways) generates OpenAPI spec with missing `default` declarations.

Also, when/if fields are marked into `required` varies a bit.

Didn't test what happens when requests are sent - are the defaults actually picked up  runtime. But at least the OpenAPI schema generation fails.

(The docs don't mention _how_ these are supported so a bit difficult to know how it's _intended_.)

### MCVE

```python
import dataclasses
import json
from dataclasses import dataclass
from typing import Annotated

import msgspec
from litestar import post
from litestar.app import Litestar
from litestar.params import Parameter
from msgspec import Struct
from pydantic import BaseModel, Field


class PydanticBody(BaseModel):
    field1: Annotated[str, Field(default="dummy")]
    field2: str = Field(default="dummy")
    field3: str = "dummy"
    field4: Annotated[str, Parameter(default="dummy")]


@dataclass
class DataclassBody:
    field1: Annotated[str, Parameter(default="dummy")]  # default generated, but declared as required
    field3: str = Parameter(default="dummy")
    field4: str = "dummy"
    field5: str = dataclasses.field(default="dummy")


class MsgspecBody(Struct):
    field1: Annotated[str, Parameter(default="dummy")]  # default generated, but declared as required
    field2: Annotated[str, msgspec.field(default="dummy")]  # no default, marked as required
    field3: str = Parameter(default="dummy")
    field4: str = "dummy"
    field5: str = msgspec.field(default="dummy")


@post("/1")
async def pydantic_handler(data: PydanticBody) -> None: ...


@post("/2")
async def dataclass_handler(data: DataclassBody) -> None: ...


@post("/3")
async def msgspec_handler(data: MsgspecBody) -> None: ...


app = Litestar([pydantic_handler, dataclass_handler, msgspec_handler])

print(json.dumps(app.openapi_schema.to_schema(), indent=4))
```


### Steps to reproduce

This generates:

```json
    "components": {
        "schemas": {
            "DataclassBody": {
                "properties": {
                    "field1": {
                        "type": "string",
                        "default": "dummy"
                    },
                    "field3": {
                        "type": "string"
                    },
                    "field4": {
                        "type": "string"
                    },
                    "field5": {
                        "type": "string"
                    }
                },
                "type": "object",
                "required": [
                    "field1"
                ],
                "title": "DataclassBody"
            },
            "MsgspecBody": {
                "properties": {
                    "field1": {
                        "type": "string",
                        "default": "dummy"
                    },
                    "field2": {
                        "type": "string"
                    },
                    "field3": {
                        "type": "string"
                    },
                    "field4": {
                        "type": "string"
                    },
                    "field5": {
                        "type": "string"
                    }
                },
                "type": "object",
                "required": [
                    "field1",
                    "field2"
                ],
                "title": "MsgspecBody"
            },
            "PydanticBody": {
                "properties": {
                    "field1": {
                        "type": "string"
                    },
                    "field2": {
                        "type": "string"
                    },
                    "field3": {
                        "type": "string"
                    },
                    "field4": {
                        "type": "string"
                    }
                },
                "type": "object",
                "required": [
                    "field4"
                ],
                "title": "PydanticBody"
            }
        }
```

### Litestar Version

2.7.0

### 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/3201">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3201/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3201/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
We use the APIs provided by `dataclasses`, `msgspec`, and `pydantic` to parse the details of a model such as the default. Both, `msgspec` and `dataclasses`, do not support specifying the `field` within the annotation but rather, you have to assign it whereas pydantic does (I think it does).

Also, I don't think `Parameter` is meant to be used for the request/response bodies. They're meant to be used to give extra details for parameters like query params, path params etc.
So _how_ should I specify defaults and other OpenAPI data, then, for those cases? Quite puzzled at this point.
It seems I was mistaken since the default isn't being generated as part of the schema even when specifying the default value correctly.

```python
from dataclasses import dataclass

from litestar import post
from litestar.app import Litestar
from litestar.openapi.spec import Schema


@dataclass
class DataclassBody:
    field4: str = "dummy"


@post("/2")
async def dataclass_handler(data: DataclassBody) -> None:
    ...


app = Litestar([dataclass_handler])


schema = app.openapi_schema.components.schemas["DataclassBody"]

assert schema.properties
schema = schema.properties["field4"]

assert isinstance(schema, Schema)
assert schema.default == "dummy" # This fails.
```

There's a few things going on here (some of which have also been discussed elsewhere I think):

```python
@dataclass
class DataclassBody:
    field1: Annotated[str, Parameter(default="dummy")]  # default generated, but declared as required
    field3: str = Parameter(default="dummy")
    field4: str = "dummy"
    field5: str = dataclasses.field(default="dummy")
```

- `field1` is simply an invalid way of defining a default for a dataclass. You are essentially "lying" to Litestar by telling it "even though I have not defined a default for this field, I want you to say that it has a default value of `"dummy"`". 
- `field3` is setting a default value for the field, but, according to dataclass semantics, the default value is `Parameter(default="dummy")`. Since this is neither an annotation, nor the default value you meant to specify, it's not surprising this isn't working
- `field4` and `field5` should work, and it not working is a bug (and I think a regression, will have to check this out)

```python
class MsgspecBody(Struct):
    field1: Annotated[str, Parameter(default="dummy")]  # default generated, but declared as required
    field2: Annotated[str, msgspec.field(default="dummy")]  # no default, marked as required
    field3: str = Parameter(default="dummy")
    field4: str = "dummy"
    field5: str = msgspec.field(default="dummy")
```
- `field1`/`field2`/`field3`: Same as with the dataclass, this is not a valid way to define a default way for a msgspec.Struct
- `field4`/`field5`: Also same as with the dataclass, this should work

It's important to keep in mind that while Litestar does allow to *extend* modelling capabilities of the chosen libraries, it does not *alter* them. You still have to adhere to the semantics and patterns these libraries use and produce a valid model for Litestar to use.

Resolving patch:
diff --git a/litestar/_openapi/schema_generation/plugins/dataclass.py b/litestar/_openapi/schema_generation/plugins/dataclass.py
--- a/litestar/_openapi/schema_generation/plugins/dataclass.py
+++ b/litestar/_openapi/schema_generation/plugins/dataclass.py
@@ -1,9 +1,11 @@
 from __future__ import annotations
 
+import dataclasses
 from dataclasses import MISSING, fields
 from typing import TYPE_CHECKING
 
 from litestar.plugins import OpenAPISchemaPlugin
+from litestar.types import Empty
 from litestar.typing import FieldDefinition
 from litestar.utils.predicates import is_optional_union
 
@@ -31,6 +33,11 @@ def to_openapi_schema(self, field_definition: FieldDefinition, schema_creator: S
                 )
             ),
             property_fields={
-                field.name: FieldDefinition.from_kwarg(type_hints[field.name], field.name) for field in dataclass_fields
+                field.name: FieldDefinition.from_kwarg(
+                    annotation=type_hints[field.name],
+                    name=field.name,
+                    default=field.default if field.default is not dataclasses.MISSING else Empty,
+                )
+                for field in dataclass_fields
             },
         )
diff --git a/litestar/_openapi/schema_generation/plugins/struct.py b/litestar/_openapi/schema_generation/plugins/struct.py
--- a/litestar/_openapi/schema_generation/plugins/struct.py
+++ b/litestar/_openapi/schema_generation/plugins/struct.py
@@ -2,6 +2,7 @@
 
 from typing import TYPE_CHECKING
 
+import msgspec
 from msgspec import Struct
 from msgspec.structs import fields
 
@@ -38,7 +39,11 @@ def is_field_required(field: FieldInfo) -> bool:
                 ]
             ),
             property_fields={
-                field.encode_name: FieldDefinition.from_kwarg(type_hints[field.name], field.encode_name)
+                field.encode_name: FieldDefinition.from_kwarg(
+                    type_hints[field.name],
+                    field.encode_name,
+                    default=field.default if field.default not in {msgspec.NODEFAULT, msgspec.UNSET} else Empty,
+                )
                 for field in struct_fields
             },
         )


Test patch:
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import dataclasses
 from dataclasses import dataclass
 from types import ModuleType
 from typing import Callable, Generic, Optional, TypeVar, cast
@@ -184,6 +185,45 @@ async def example_route() -> Lookup:
         }
 
 
+def test_dataclass_field_default() -> None:
+    # https://github.com/litestar-org/litestar/issues/3201
+    @dataclass
+    class SomeModel:
+        field_a: str = "default_a"
+        field_b: str = dataclasses.field(default="default_b")
+        field_c: str = dataclasses.field(default_factory=lambda: "default_c")
+
+    @get("/")
+    async def handler() -> SomeModel:
+        return SomeModel()
+
+    app = Litestar(route_handlers=[handler], signature_types=[SomeModel])
+    schema = app.openapi_schema.components.schemas["test_dataclass_field_default.SomeModel"]
+    assert schema
+    assert schema.properties["field_a"].default == "default_a"  # type: ignore[union-attr, index]
+    assert schema.properties["field_b"].default == "default_b"  # type: ignore[union-attr, index]
+    assert schema.properties["field_c"].default is None  # type: ignore[union-attr, index]
+
+
+def test_struct_field_default() -> None:
+    # https://github.com/litestar-org/litestar/issues/3201
+    class SomeModel(msgspec.Struct, kw_only=True):
+        field_a: str = "default_a"
+        field_b: str = msgspec.field(default="default_b")
+        field_c: str = msgspec.field(default_factory=lambda: "default_c")
+
+    @get("/")
+    async def handler() -> SomeModel:
+        return SomeModel()
+
+    app = Litestar(route_handlers=[handler], signature_types=[SomeModel])
+    schema = app.openapi_schema.components.schemas["test_struct_field_default.SomeModel"]
+    assert schema
+    assert schema.properties["field_a"].default == "default_a"  # type: ignore[union-attr, index]
+    assert schema.properties["field_b"].default == "default_b"  # type: ignore[union-attr, index]
+    assert schema.properties["field_c"].default is None  # type: ignore[union-attr, index]
+
+
 def test_schema_for_optional_path_parameter() -> None:
     @get(path=["/", "/{test_message:str}"], media_type=MediaType.TEXT, sync_to_thread=False)
     def handler(test_message: Optional[str]) -> str:  # noqa: UP007