#multipart form data payload with single array element

1 messages · Page 1 of 1 (latest)

limber canopy
#

I am having an issue with multipart form data with a single array element.
Let's say the endpoint signature and deserializer look like this:

@patch(path="/l", description="qew.")
async def DP(self, request: "Request", db_session: AsyncSession, data: Annotated[DPPayload, Body(media_type=RequestEncodingType.MULTI_PART)]) -> DPPayload | Response:
    ...

class PetsEnum(enum.IntEnum):
    DOG = 1
    CAT = 2
    FISH = 3
    BIRD = 4
    RABBIT = 5
    HAMSTER = 6
    GUINEA_PIG = 7
    SNAKE = 8
    TURTLE = 9
    LIZARD = 10
    OTHER = 11

class DPPayload(msgspec.Struct, kw_only=True, omit_defaults=True, repr_omit_defaults=True):
    # Multiple choice fields
    pets: Optional[Annotated[list[PetsEnum], msgspec.Meta(max_length=4)]] | msgspec.UnsetType = msgspec.UNSET
    speaks: Optional[list[Annotated[str, msgspec.Meta(max_length=2, pattern=r'^[a-z]+$')]]] | msgspec.UnsetType = msgspec.UNSET

When I send a patch request with the following data:

pets: 3
pets: 4

It doesn't fail, and the data object at the endpoint contains a key pets with a list.

However, if I send:

pets: 2

it will be converted to dict {'pets': 2} which is completly fine and not wrong.

It (probably) fails the dependencies check in http.py at the method async def _get_response_data(...) for DPPayload.

Does anyone have an idea or solution for this?

teal crestBOT
#
Notes for multipart form data payload with single array element
At your assistance

@limber canopy

No Response?

If no response in a reasonable time, ping @Member.

Closing

To close, type !solve or byte solve.

MCVE

Please include an MCVE so that we can reproduce your issue locally.

limber canopy
#

An easy workaround would be to add int and str respectively for pets and speaks in the DPPayload class:

class DPPayload(msgspec.Struct, kw_only=True, omit_defaults=True, repr_omit_defaults=True):
    # Multiple choice fields
    pets: Optional[Annotated[list[PetsEnum], msgspec.Meta(max_length=4)]] | int | msgspec.UnsetType = msgspec.UNSET
    speaks: Optional[list[Annotated[str, msgspec.Meta(max_length=2, pattern=r'^[a-z]+$')]]] | str | msgspec.UnsetType = msgspec.UNSET

But at the endpoint, I really want to handle this as a list without resorting to "magic tricks" with if-else statements, like:

if isinstance(value, list):
    for lang in value:
        if lang.lower() not in iso_639_1_codes:
            return Response(content={"detail": "Language not supported", "code": "400"}, status_code=400, media_type=MediaType.JSON)

if isinstance(value, str):
    if value not in iso_639_1_codes:
        return Response(content={"detail": "Language not supported", "code": "400"}, status_code=400, media_type=MediaType.JSON)
    value = [value]

Not 100% (maybe just cope I don't know), but if I recall correctly some framework have array like syntax for this type of problem:

pets[]: 2

Would intepret it as dict with pets key and list value of 2.

#

Currently I use this one:

@staticmethod
    async def _get_response_data(
        route_handler: HTTPRouteHandler, parameter_model: KwargsModel, request: Request
    ) -> tuple[Any, DependencyCleanupGroup | None]:
        """Determine what kwargs are required for the given route handler's ``fn`` and calls it."""
        parsed_kwargs: dict[str, Any] = {}
        cleanup_group: DependencyCleanupGroup | None = None

        if parameter_model.has_kwargs and route_handler.signature_model:
            kwargs = parameter_model.to_kwargs(connection=request)

            if "data" in kwargs:
                try:
                    data = await kwargs["data"]
                except SerializationException as e:
                    raise ClientException(str(e)) from e

                if data is Empty:
                    del kwargs["data"]
                else:
                    processed_data = {} # <---------------------------- New part START
                    for key, value in data.items():
                        if key.endswith('[]') and len(key) > 2:
                            base_key = key[:-2]
                            if base_key in processed_data:
                                if isinstance(processed_data[base_key], list):
                                    processed_data[base_key].append(value)
                                else:
                                    processed_data[base_key] = [processed_data[base_key], value]
                            else:
                                processed_data[base_key] = [value]
                        else:
                            processed_data[key] = value

                    kwargs["data"] = processed_data # <---------------------------- New part END

            if "body" in kwargs:
                kwargs["body"] = await kwargs["body"]



.....

It would be more appropriate to do these things probably in parameter_model.to_kwargs at the extractor level, but I'm not sure

upper lion
#

multipart does not have the notion of "single element arrays". So if you do want to support them, you can either force everything to be a list of values, or you have to do some non-standard stuff (e.g. using the [] notation)

#

Since forcing every field to be a list of values would be kind of annoying, Litestar collapses single values if the field permits it