Repository: litestar-org/litestar

Issue:
DTO backend does not transferring mappings with nested models
### Reported by
[Alc](https://discord.com/users/314787529100361748) in Discord: DTO Unexpected Parsing Error

### Description
Route with DTO does not parse the input data, route without DTO does well, the tests below currently pass, but the one that raises 400 should ideally be 201

### MCVE
```py
from litestar import post
from litestar.testing import create_test_client
from litestar.contrib.pydantic import PydanticDTO
from pydantic import BaseModel

class Inner(BaseModel):
    i1: int
    i2: int

class Outer(BaseModel):
    inner: dict[str, Inner]

@post(dto=PydanticDTO[Outer])
async def something(data: Outer) -> Outer:
    return data

@post('no_dto')
async def something_no_dto(data: Outer) -> Outer:
    return data

with create_test_client([something, something_no_dto]) as client:
    json = {
        "inner": {
            "additionalProp1": {"i1": 0, "i2": 0},
            "additionalProp2": {"i1": 0, "i2": 0},
            "additionalProp3": {"i1": 0, "i2": 0},
        }
    }
    response = client.post("", json=json)
    assert response.status_code == 400

    response = client.post("no_dto", json=json)
    assert response.status_code == 201
```

### Logs
~~no one reads logs~~

### Litestar Version
main

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



Discussion:
@peterschutt FYI, I opened the original issue in discord. If you want me to test a build, please let me know here.
@JacobCoffee is it possible for byte to link to the message in discord that these originate from?
> @JacobCoffee is it possible for byte to link to the message in discord that these originate from?

I believe @Alc-Alc resolved the issue I'd created in discord after opening this issue, I can't find it anymore. Let me know if you need more information.

Resolving patch:
diff --git a/litestar/dto/_backend.py b/litestar/dto/_backend.py
--- a/litestar/dto/_backend.py
+++ b/litestar/dto/_backend.py
@@ -701,6 +701,24 @@ def _transfer_type_data(
             )
 
         return transfer_type.field_definition.instantiable_origin(source_value)
+
+    if isinstance(transfer_type, MappingType):
+        if transfer_type.has_nested:
+            return transfer_type.field_definition.instantiable_origin(
+                (
+                    key,
+                    _transfer_type_data(
+                        source_value=value,
+                        transfer_type=transfer_type.value_type,
+                        nested_as_dict=False,
+                        is_data_field=is_data_field,
+                    ),
+                )
+                for key, value in source_value.items()
+            )
+
+        return transfer_type.field_definition.instantiable_origin(source_value)
+
     return source_value
 
 
diff --git a/litestar/dto/_codegen_backend.py b/litestar/dto/_codegen_backend.py
--- a/litestar/dto/_codegen_backend.py
+++ b/litestar/dto/_codegen_backend.py
@@ -24,6 +24,7 @@
 from litestar.dto._types import (
     CollectionType,
     CompositeType,
+    MappingType,
     SimpleType,
     TransferDTOFieldDefinition,
     TransferType,
@@ -506,6 +507,21 @@ def _create_transfer_type_data_body(
             self._add_stmt(f"{assignment_target} = {origin_name}({source_value_name})")
             return
 
+        if isinstance(transfer_type, MappingType):
+            origin_name = self._add_to_fn_globals("origin", transfer_type.field_definition.instantiable_origin)
+            if transfer_type.has_nested:
+                transfer_type_data_fn = TransferFunctionFactory.create_transfer_type_data(
+                    is_data_field=self.is_data_field, transfer_type=transfer_type.value_type
+                )
+                transfer_type_data_name = self._add_to_fn_globals("transfer_type_data", transfer_type_data_fn)
+                self._add_stmt(
+                    f"{assignment_target} = {origin_name}((key, {transfer_type_data_name}(item)) for key, item in {source_value_name}.items())"
+                )
+                return
+
+            self._add_stmt(f"{assignment_target} = {origin_name}({source_value_name})")
+            return
+
         self._add_stmt(f"{assignment_target} = {source_value_name}")
 
     def _create_transfer_nested_union_type_data(


Test patch:
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
@@ -3,7 +3,7 @@
 
 from dataclasses import dataclass, field
 from types import ModuleType
-from typing import TYPE_CHECKING, Callable, List, Optional
+from typing import TYPE_CHECKING, Callable, Dict, List, Optional
 from unittest.mock import MagicMock
 
 import pytest
@@ -40,6 +40,7 @@ class DC:
     a: int
     nested: NestedDC
     nested_list: List[NestedDC]
+    nested_mapping: Dict[str, NestedDC]
     b: str = field(default="b")
     c: List[int] = field(default_factory=list)
     optional: Optional[str] = None
@@ -51,13 +52,20 @@ class DC:
     "c": [],
     "nested": {"a": 1, "b": "two"},
     "nested_list": [{"a": 1, "b": "two"}],
+    "nested_mapping": {"a": {"a": 1, "b": "two"}},
     "optional": None,
 }
-RAW = b'{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"b":"b","c":[],"optional":null}'
-COLLECTION_RAW = (
-    b'[{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"b":"b","c":[],"optional":null}]'
+RAW = b'{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"nested_mapping":{"a":{"a":1,"b":"two"}},"b":"b","c":[],"optional":null}'
+COLLECTION_RAW = b'[{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"nested_mapping":{"a":{"a":1,"b":"two"}},"b":"b","c":[],"optional":null}]'
+STRUCTURED = DC(
+    a=1,
+    b="b",
+    c=[],
+    nested=NestedDC(a=1, b="two"),
+    nested_list=[NestedDC(a=1, b="two")],
+    nested_mapping={"a": NestedDC(a=1, b="two")},
+    optional=None,
 )
-STRUCTURED = DC(a=1, b="b", c=[], nested=NestedDC(a=1, b="two"), nested_list=[NestedDC(a=1, b="two")], optional=None)
 
 
 @pytest.fixture(name="dto_factory")
@@ -89,7 +97,10 @@ def test_backend_parse_raw_json(
                 wrapper_attribute_name=None,
                 is_data_field=True,
                 handler_id="test",
-            ).parse_raw(b'{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}]}', asgi_connection)
+            ).parse_raw(
+                b'{"a":1,"nested":{"a":1,"b":"two"},"nested_list":[{"a":1,"b":"two"}],"nested_mapping":{"a":{"a":1,"b":"two"}}}',
+                asgi_connection,
+            )
         )
         == DESTRUCTURED
     )
@@ -112,7 +123,7 @@ def _handler() -> None: ...
                 is_data_field=True,
                 handler_id="test",
             ).parse_raw(
-                b"\x83\xa1a\x01\xa6nested\x82\xa1a\x01\xa1b\xa3two\xabnested_list\x91\x82\xa1a\x01\xa1b\xa3two",
+                b"\x87\xa1a\x01\xa6nested\x82\xa1a\x01\xa1b\xa3two\xabnested_list\x91\x82\xa1a\x01\xa1b\xa3two\xaenested_mapping\x81\xa1a\x82\xa1a\x01\xa1b\xa3two\xa1b\xa1b\xa1c\x90\xa8optional\xc0",
                 asgi_connection,
             )
         )