Repository: litestar-org/litestar

Issue:
Bug: return DTO causes field descriptions in Pydantic model to disappear
### Description

When using a Pydantic model without a return DTO, the field descriptions show up correctly in /schema documentation. With a return_dto, field descriptions disappear.

## No DTO - Field Descriptions Present
_See bottom portion of image below data types for `id` and `user_name` ._
![No_DTO_Field_descriptions_present](https://github.com/litestar-org/litestar/assets/143021367/bee60d67-e39b-4b84-b674-8c68678881fd)

## With return DTO - Field Descriptions now missing
![Return_DTO_Field_descriptions_missing](https://github.com/litestar-org/litestar/assets/143021367/a2b8ff09-69ec-41f8-8c33-d04c6b747244)


### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Annotated

from litestar import Litestar, get
from litestar.contrib.pydantic import PydanticDTO
from litestar.dto import DTOConfig
from pydantic import BaseModel, Field


class User(BaseModel):
    id: Annotated[
        int,
        Field(description="This is a test (id description)."),
    ]

    user_name: str = Field(description="Not annotated test (name description)")


class DataCollectionDTO(PydanticDTO[User]):
    config = DTOConfig(rename_strategy="camel")


USER_DB = {1: {"id": 1, "user_name": "Stella"}}


# #1 - NO DTO: field descriptions will be present in /schema
@get("/user/{user_id:int}", sync_to_thread=False)
# #2 - With return_DTO: field descriptions are missing from schema
# @get("/user/{user_id:int}", return_dto=DataCollectionDTO, sync_to_thread=False)
async def get_user(user_id: int) -> User:
    return USER_DB[user_id]


app = Litestar(route_handlers=[get_user])
```


### Steps to reproduce

```bash
1. Build and run a litestar application using the code above for app.py.
2. View the /schema for the API (likely at http://127.0.0.1:8000/schema).
3. See the field descriptions for id and user_name as shown in the top image above. Field descriptions should be present
4. Edit app.py by commenting out the first decorator for get_user() and removing the comment from the second decorator line.
5. Again run the litestar application and view the /schema for the API.
6. Now the field descriptions are missing as in the second image above.
```


### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

Litestar 2.4.2  (running on Ubuntu in WSL2)

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



Discussion:

Resolving patch:
diff --git a/litestar/dto/_backend.py b/litestar/dto/_backend.py
--- a/litestar/dto/_backend.py
+++ b/litestar/dto/_backend.py
@@ -750,14 +750,17 @@ def _create_struct_field_meta_for_field_definition(field_definition: TransferDTO
         return None
 
     return msgspec.Meta(
-        gt=kwarg_definition.gt,
+        description=kwarg_definition.description,
+        examples=[e.value for e in kwarg_definition.examples or []],
         ge=kwarg_definition.ge,
-        lt=kwarg_definition.lt,
+        gt=kwarg_definition.gt,
         le=kwarg_definition.le,
-        multiple_of=kwarg_definition.multiple_of,
-        min_length=kwarg_definition.min_length if not field_definition.is_partial else None,
+        lt=kwarg_definition.lt,
         max_length=kwarg_definition.max_length if not field_definition.is_partial else None,
+        min_length=kwarg_definition.min_length if not field_definition.is_partial else None,
+        multiple_of=kwarg_definition.multiple_of,
         pattern=kwarg_definition.pattern,
+        title=kwarg_definition.title,
     )
 
 


Test patch:
diff --git a/tests/unit/test_contrib/test_pydantic/test_dto.py b/tests/unit/test_contrib/test_pydantic/test_dto.py
--- a/tests/unit/test_contrib/test_pydantic/test_dto.py
+++ b/tests/unit/test_contrib/test_pydantic/test_dto.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, cast
 
 import pytest
 from pydantic import v1 as pydantic_v1
@@ -13,8 +13,13 @@
 from litestar.typing import FieldDefinition
 
 if TYPE_CHECKING:
+    from collections.abc import Callable
+    from types import ModuleType
+
     from pydantic import BaseModel
 
+    from litestar import Litestar
+
 
 def test_schema_required_fields_with_pydantic_dto(
     use_experimental_dto_backend: bool, base_model: type[BaseModel]
@@ -62,3 +67,36 @@ class Model(pydantic_v1.BaseModel):
     dto_type = PydanticDTO[Model]
     assert dto_type.detect_nested_field(FieldDefinition.from_annotation(Model)) is True
     assert dto_type.detect_nested_field(FieldDefinition.from_annotation(int)) is False
+
+
+def test_pydantic_field_descriptions(create_module: Callable[[str], ModuleType]) -> None:
+    module = create_module(
+        """
+from litestar import Litestar, get
+from litestar.contrib.pydantic import PydanticDTO
+from litestar.dto import DTOConfig
+from pydantic import BaseModel, Field
+from typing_extensions import Annotated
+
+class User(BaseModel):
+    id: Annotated[
+        int,
+        Field(description="This is a test (id description)."),
+    ]
+
+class DataCollectionDTO(PydanticDTO[User]):
+    config = DTOConfig(rename_strategy="camel")
+
+@get("/user", return_dto=DataCollectionDTO, sync_to_thread=False)
+def get_user() -> User:
+    return User(id=user_id)
+
+app = Litestar(route_handlers=[get_user])
+        """
+    )
+    app = cast("Litestar", module.app)
+    schema = app.openapi_schema
+    assert schema.components.schemas is not None
+    component_schema = schema.components.schemas["GetUserUserResponseBody"]
+    assert component_schema.properties is not None
+    assert component_schema.properties["id"].description == "This is a test (id description)."
diff --git a/tests/unit/test_dto/test_factory/test_backends/test_backends.py b/tests/unit/test_dto/test_factory/test_backends/test_backends.py
--- a/tests/unit/test_dto/test_factory/test_backends/test_backends.py
+++ b/tests/unit/test_dto/test_factory/test_backends/test_backends.py
@@ -7,18 +7,20 @@
 from unittest.mock import MagicMock
 
 import pytest
-from msgspec import Struct, to_builtins
+from msgspec import Meta, Struct, to_builtins
 
 from litestar import Litestar, Request, get, post
 from litestar._openapi.schema_generation import SchemaCreator
 from litestar.dto import DataclassDTO, DTOConfig, DTOField
-from litestar.dto._backend import DTOBackend
+from litestar.dto._backend import DTOBackend, _create_struct_field_meta_for_field_definition
 from litestar.dto._types import CollectionType, SimpleType, TransferDTOFieldDefinition
 from litestar.dto.data_structures import DTOFieldDefinition
 from litestar.enums import MediaType
 from litestar.exceptions import SerializationException
+from litestar.openapi.spec.example import Example
 from litestar.openapi.spec.reference import Reference
 from litestar.openapi.spec.schema import Schema
+from litestar.params import KwargDefinition
 from litestar.serialization import encode_json
 from litestar.testing import RequestFactory
 from litestar.typing import FieldDefinition
@@ -448,3 +450,29 @@ class Factory(DataclassDTO):
     assert b_d_nested_info is not None
     assert not next(f for f in b_d_nested_info.field_definitions if f.name == "e").is_excluded
     assert b_d_nested_info.field_definitions[1].name == "f"
+
+
+@pytest.mark.parametrize(
+    ("constraint_kwargs",),
+    (
+        ({},),
+        ({"gt": 0, "lt": 2},),
+        ({"ge": 0, "le": 2},),
+        ({"min_length": 1, "max_length": 2},),
+        ({"pattern": "test"},),
+    ),
+)
+def test_create_struct_field_meta_for_field_definition(constraint_kwargs: Any) -> None:
+    mock_field = MagicMock(spec=TransferDTOFieldDefinition, is_partial=False)
+    mock_field.kwarg_definition = KwargDefinition(
+        description="test",
+        examples=[Example(value=1)],
+        title="test",
+        **constraint_kwargs,
+    )
+    assert _create_struct_field_meta_for_field_definition(mock_field) == Meta(
+        description="test",
+        examples=[1],
+        title="test",
+        **constraint_kwargs,
+    )