Repository: litestar-org/litestar

Issue:
Bug: forms validation discrepency between underlying object and form type
### Description

Given this little app that basically enables in a browser to post a form to 6 different routes:  atrts/msgspec/pydantic times url-encoded/multi-part version, I have isues to understand why the multi-part pydantic vs attrs/msgspec different behaviour. 

I would expect a 201 on all and get a 400 on multi-part msgspec and attrs (see logs), is that intended ?

Input I pass is purposedely 1 and 1 for name and amount and the same for all POST logs below.



### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Annotated

import msgspec
from attr import define, field
from attr.validators import ge, instance_of, lt
from jinja2 import Environment, DictLoader
from pydantic import BaseModel, ConfigDict, Field

from litestar import Litestar, Request, post, get
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.enums import RequestEncodingType
from litestar.params import Body
from litestar.response import Template
from litestar.template import TemplateConfig

MAX_INT_POSTGRES = 10


@define
class AddProductFormAttrs:
    name: str
    amount: int = field(validator=[instance_of(int), ge(1), lt(MAX_INT_POSTGRES)])


class AddProductFormPydantic(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    name: str
    amount: int = Field(ge=1, lt=MAX_INT_POSTGRES)


class AddProductFormMsgspec(msgspec.Struct):
    name: str
    amount: Annotated[int, msgspec.Meta(lt=MAX_INT_POSTGRES, ge=1)]


@get(path="/")
async def form_index() -> Template:
    return Template(template_name="form.html", context={"templates": ["attrs", "pydantic", "msgspec"]})


@post(path="/form/attrs")
async def form_attrs(
        request: Request, data: Annotated[AddProductFormAttrs, Body(media_type=RequestEncodingType.URL_ENCODED)]
) -> int:
    print(type(data.name))
    return data.amount


@post(path="/form/pydantic")
async def form_pydantic(
        request: Request, data: Annotated[AddProductFormPydantic, Body(media_type=RequestEncodingType.URL_ENCODED)]
) -> int:
    print(type(data.name))
    return data.amount


@post(path="/form/msgspec")
async def form_msgspec(
        request: Request, data: Annotated[AddProductFormMsgspec, Body(media_type=RequestEncodingType.URL_ENCODED)]
) -> int:
    print(type(data.name))
    return data.amount

@post(path="/form/multipart/attrs")
async def form_multipart_attrs(
    request: Request, data: Annotated[AddProductFormAttrs, Body(media_type=RequestEncodingType.MULTI_PART)]
) -> int:
    print(type(data.name))
    return data.amount


@post(path="/form/multipart/pydantic")
async def form_multipart_pydantic(
    request: Request, data: Annotated[AddProductFormPydantic, Body(media_type=RequestEncodingType.MULTI_PART)]
) -> int:
    print(type(data.name))
    return data.amount


@post(path="/form/multipart/msgspec")
async def form_multipart_msgspec(
        request: Request, data: Annotated[AddProductFormMsgspec, Body(media_type=RequestEncodingType.MULTI_PART)]
) -> int:
    print(type(data.name))
    return data.amount

form_html = """
<html>
<body>
{% for t in templates %}
    <div>{{ t }}
    <form action="/form/{{ t }}" method="post">
    <input type="text" name="name"/>
    <input type="number" name="amount"/>
    <button type="submit">Submit</button>
    </form>
    </div>
    <div>{{ t }} multipart
    <form action="/form/multipart//{{ t }}" method="post" enctype="multipart/form-data">
    <input type="text" name="name"/>
    <input type="number" name="amount"/>
    <button type="submit">Submit</button>
    </form>
    </div>
{% endfor %}
</body>
</html>
"""

environment = Environment(loader=DictLoader({"form.html": form_html}))
app = Litestar(route_handlers=[form_index, form_attrs, form_pydantic, form_msgspec, form_multipart_attrs, form_multipart_pydantic, form_multipart_msgspec],
               template_config=TemplateConfig(JinjaTemplateEngine.from_environment(jinja_environment=environment)),
               debug=True)
```

```
```


### Steps to reproduce

_No response_

### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

```bash
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [29797]
INFO:     Started server process [29878]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     127.0.0.1:48782 - "GET / HTTP/1.1" 200 OK
<class 'str'>
INFO:     127.0.0.1:48782 - "POST /form/multipart/pydantic HTTP/1.1" 201 Created
INFO:     127.0.0.1:55766 - "GET / HTTP/1.1" 200 OK
<class 'str'>
INFO:     127.0.0.1:59238 - "POST /form/attrs HTTP/1.1" 201 Created
INFO:     127.0.0.1:56858 - "POST /form/multipart/attrs HTTP/1.1" 400 Bad Request
ERROR - 2023-09-04 14:24:31,1613157331 - litestar - middleware - exception raised on http connection to route /form/multipart/attrs

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/attrs
Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/attrs
<class 'str'>
INFO:     127.0.0.1:56870 - "POST /form/pydantic HTTP/1.1" 201 Created
<class 'str'>
INFO:     127.0.0.1:52932 - "POST /form/multipart/pydantic HTTP/1.1" 201 Created
<class 'str'>
INFO:     127.0.0.1:56944 - "POST /form/msgspec HTTP/1.1" 201 Created
INFO:     127.0.0.1:56944 - "POST /form/multipart/msgspec HTTP/1.1" 400 Bad Request
ERROR - 2023-09-04 14:24:57,1613183344 - litestar - middleware - exception raised on http connection to route /form/multipart/msgspec

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/msgspec
Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/msgspec
WARNING:  StatReload detected changes in 'te.py'. Reloading...
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [29878]
INFO:     Started server process [30406]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     127.0.0.1:50626 - "GET / HTTP/1.1" 200 OK
<class 'str'>
INFO:     127.0.0.1:48508 - "POST /form/attrs HTTP/1.1" 201 Created
<class 'str'>
INFO:     127.0.0.1:48508 - "POST /form/pydantic HTTP/1.1" 201 Created
<class 'str'>
INFO:     127.0.0.1:48508 - "POST /form/msgspec HTTP/1.1" 201 Created
INFO:     127.0.0.1:35918 - "POST /form/multipart/attrs HTTP/1.1" 400 Bad Request
ERROR - 2023-09-04 14:26:37,1613283298 - litestar - middleware - exception raised on http connection to route /form/multipart/attrs

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/attrs
Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/attrs
<class 'str'>
INFO:     127.0.0.1:55018 - "POST /form/multipart/pydantic HTTP/1.1" 201 Created
INFO:     127.0.0.1:51360 - "POST /form/multipart/msgspec HTTP/1.1" 400 Bad Request
ERROR - 2023-09-04 14:26:58,1613303675 - litestar - middleware - exception raised on http connection to route /form/multipart/msgspec

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/msgspec
Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `str`, got `int` - at `$.data.name`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lotso/PycharmProjects/litestar/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lotso/PycharmProjects/litestar/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.ValidationException: 400: Validation failed for POST http://localhost:8000/form/multipart/msgspec
WARNING:  StatReload detected changes in 'te.py'. Reloading...
```


### Litestar Version

```
❯ litestar version
2.0.0beta4
```

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

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

StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
whats the input you are sending?
> whats the input you are sending?

the short version of the log above is that I'm entering 1 for both the input `name` and input `amount` in the form.

what is strange is that for url-encoded forms I get a 201 on all 3 endpoints, the data.name is a string as expected.

but as soon as you have multi-part forms, for the same inputs, the pydantic is the only one returning a 201, others return a 400 complaining data.name is not a string

if I take as an example the multipart vs url-encoded using the msgspec Struct only, by the time we reach this line:
https://github.com/litestar-org/litestar/blob/c36dba249337062266fe2175a256ed7de099fa0e/litestar/_signature/model.py#L190

what we have when entering both times 1 and 1 in the browser.

1. in the url-encoded case is
`{'request': <litestar.connection.request.Request object at 0x7f955cf165c0>, 'data': {'amount': '1', 'name': '1'}}
`
2. vs for the multi-part case
`{'request': <litestar.connection.request.Request object at 0x7f955cf16fc0>, 'data': {'name': 1, 'amount': 1}}`
Well, the difference between url-encoded and multipart is that for url encoded we parse the raw request to a dict of string/string key value pairs and then let msgspec, attrs, pydantic etc. to handle validation and parsing. For multipart we use regex, and I think it's probable we do some conversion there. 

We might need to ensure everything remains a string as well. 
from briefly looking at it, I think this might be due to this line in https://github.com/litestar-org/litestar/blob/c36dba249337062266fe2175a256ed7de099fa0e/litestar/_multipart.py#L160

if you replace it with `fields[field_name].append(post_data.decode())` then a 201 occurs.

I think the decoding using msgspec is too early,

will try a PR with this and see how it afffects the tests :)
> from briefly looking at it, I think this might be due to this line in https://github.com/litestar-org/litestar/blob/c36dba249337062266fe2175a256ed7de099fa0e/litestar/_multipart.py#L160
> 
> if you replace it with `fields[field_name].append(post_data.decode())` then a 201 occurs.
> 
> I think the decoding using msgspec is too early,
> 
> will try a PR with this and see how it afffects the tests :)

Great 👍
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/_multipart.py b/litestar/_multipart.py
--- a/litestar/_multipart.py
+++ b/litestar/_multipart.py
@@ -31,8 +31,7 @@
 from urllib.parse import unquote
 
 from litestar.datastructures.upload_file import UploadFile
-from litestar.exceptions import SerializationException, ValidationException
-from litestar.serialization import decode_json
+from litestar.exceptions import ValidationException
 
 __all__ = ("parse_body", "parse_content_header", "parse_multipart_form")
 
@@ -156,10 +155,7 @@ def parse_multipart_form(
                 )
                 fields[field_name].append(form_file)
             elif post_data:
-                try:
-                    fields[field_name].append(decode_json(post_data, type_decoders=type_decoders))
-                except SerializationException:
-                    fields[field_name].append(post_data.decode(content_charset))
+                fields[field_name].append(post_data.decode(content_charset))
             else:
                 fields[field_name].append(None)
 


Test patch:
diff --git a/tests/unit/test_kwargs/test_multipart_data.py b/tests/unit/test_kwargs/test_multipart_data.py
--- a/tests/unit/test_kwargs/test_multipart_data.py
+++ b/tests/unit/test_kwargs/test_multipart_data.py
@@ -5,18 +5,20 @@
 from pathlib import Path
 from typing import Any, DefaultDict, Dict, List, Optional
 
+import msgspec
 import pytest
-from pydantic import BaseConfig, BaseModel
+from attr import define, field
+from attr.validators import ge, instance_of, lt
+from pydantic import BaseConfig, BaseModel, ConfigDict, Field
 from typing_extensions import Annotated
 
 from litestar import Request, post
-from litestar.contrib.pydantic import _model_dump, _model_dump_json
+from litestar.contrib.pydantic import _model_dump
 from litestar.datastructures.upload_file import UploadFile
 from litestar.enums import RequestEncodingType
 from litestar.params import Body
 from litestar.status_codes import HTTP_201_CREATED, HTTP_400_BAD_REQUEST
 from litestar.testing import create_test_client
-from tests import PydanticPerson, PydanticPersonFactory
 
 from . import Form
 
@@ -100,26 +102,24 @@ def test_method(data: Annotated[t_type, Body(media_type=RequestEncodingType.MULT
 
 
 def test_request_body_multi_part_mixed_field_content_types() -> None:
-    person = PydanticPersonFactory.build()
-
     @dataclass
     class MultiPartFormWithMixedFields:
         image: UploadFile
         tags: List[int]
-        profile: PydanticPerson
 
     @post(path="/form")
     async def test_method(data: MultiPartFormWithMixedFields = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
         file_data = await data.image.read()
         assert file_data == b"data"
         assert data.tags == [1, 2, 3]
-        assert data.profile == person
 
     with create_test_client(test_method) as client:
         response = client.post(
             "/form",
             files={"image": ("image.png", b"data")},
-            data={"tags": ["1", "2", "3"], "profile": _model_dump_json(person)},
+            data={
+                "tags": ["1", "2", "3"],
+            },
         )
         assert response.status_code == HTTP_201_CREATED
 
@@ -449,7 +449,7 @@ async def hello_world(
 class ProductForm:
     name: str
     int_field: int
-    options: List[int]
+    options: str
     optional_without_default: Optional[float]
     optional_with_default: Optional[int] = None
 
@@ -490,41 +490,61 @@ def handler(
         assert response.status_code == HTTP_201_CREATED
 
 
-def test_multipart_handling_of_none_json_lists_with_multiple_elements() -> None:
-    @post("/")
-    def handler(
-        data: Annotated[ProductForm, Body(media_type=RequestEncodingType.MULTI_PART)],
-    ) -> None:
-        assert data
+MAX_INT_POSTGRES = 10
 
-    with create_test_client(route_handlers=[handler]) as client:
-        response = client.post(
-            "/",
-            content=(
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="name"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n"
-                b"moishe zuchmir\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="int_field"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n"
-                b"1\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="options"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n"
-                b"1\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="options"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n"
-                b"2\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="optional_without_default"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd\r\n"
-                b'Content-Disposition: form-data; name="optional_with_default"\r\n'
-                b"Content-Type: application/octet-stream\r\n\r\n\r\n"
-                b"--1f35df74046888ceaa62d8a534a076dd--\r\n"
-            ),
-            headers={"Content-Type": "multipart/form-data; boundary=1f35df74046888ceaa62d8a534a076dd"},
-        )
+
+@define
+class AddProductFormAttrs:
+    name: str
+    amount: int = field(validator=[instance_of(int), ge(1), lt(MAX_INT_POSTGRES)])
+
+
+class AddProductFormPydantic(BaseModel):
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+    name: str
+    amount: int = Field(ge=1, lt=MAX_INT_POSTGRES)
+
+
+class AddProductFormMsgspec(msgspec.Struct):
+    name: str
+    amount: Annotated[int, msgspec.Meta(lt=MAX_INT_POSTGRES, ge=1)]
+
+
+@pytest.mark.parametrize("form_object", [AddProductFormMsgspec, AddProductFormPydantic, AddProductFormAttrs])
+@pytest.mark.parametrize("form_type", [RequestEncodingType.URL_ENCODED, RequestEncodingType.MULTI_PART])
+def test_multipart_and_url_encoded_behave_the_same(form_object, form_type) -> None:  # type: ignore[no-untyped-def]
+    @post(path="/form")
+    async def form_(request: Request, data: Annotated[form_object, Body(media_type=form_type)]) -> int:
+        assert isinstance(data.name, str)
+        return data.amount  # type: ignore[no-any-return]
+
+    with create_test_client(
+        route_handlers=[
+            form_,
+        ]
+    ) as client:
+        if form_type == RequestEncodingType.URL_ENCODED:
+            response = client.post(
+                "/form",
+                data={
+                    "name": 1,
+                    "amount": 1,
+                },
+            )
+        else:
+            response = client.post(
+                "/form",
+                content=(
+                    b"--1f35df74046888ceaa62d8a534a076dd\r\n"
+                    b'Content-Disposition: form-data; name="name"\r\n'
+                    b"Content-Type: application/octet-stream\r\n\r\n"
+                    b"1\r\n"
+                    b"--1f35df74046888ceaa62d8a534a076dd\r\n"
+                    b'Content-Disposition: form-data; name="amount"\r\n'
+                    b"Content-Type: application/octet-stream\r\n\r\n"
+                    b"1\r\n"
+                    b"--1f35df74046888ceaa62d8a534a076dd--\r\n"
+                ),
+                headers={"Content-Type": "multipart/form-data; boundary=1f35df74046888ceaa62d8a534a076dd"},
+            )
         assert response.status_code == HTTP_201_CREATED