Repository: tiangolo/fastapi

Issue:
Support repeated key=value in form data
### Is your feature request related to a problem

Yes.

Given some URL encoded data like this...

```
choices=parrot&choices=spider
```

...only the last key=value wins.

This does not work like I expected:
```python
choices: list = Form(...)
```

You can only validate against the last value.

### The solution you would like

Perhaps FastAPI should collect repeated keys in the 2-tuple list that `request.form()` gives and assign those values as a list to the same key before validation happens.





Discussion:
Hey @StephenCarboni, parameters in the URL are part of a querystring and you can get and validate them as described here: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#query-parameter-list-multiple-values
As far as I understand, `Form()` is using only for request body
Oh, sorry, looks like I misread you message.
Anyway, I think this issue better be created in the starlette repo, because the `request` is an object from starlette.
I am a little surprised that `choices=parrot&choices=spider` doesn't work; if I recall correctly it definitely works for query parameters, and is how you would send a list of values.

The logic for form data parsing is in the `starlette.requests.Request.form` method, and delegates to [`python-multipart`](https://github.com/andrew-d/python-multipart), so you can look for the specific logic there.

(Since it uses an entirely different library for parsing, it ultimately isn't THAT surprising that the logic might be different.)

Resolving patch:
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -629,9 +629,9 @@ async def request_body_to_args(
         for field in required_params:
             value: Any = None
             if received_body is not None:
-                if field.shape in sequence_shapes and isinstance(
-                    received_body, FormData
-                ):
+                if (
+                    field.shape in sequence_shapes or field.type_ in sequence_types
+                ) and isinstance(received_body, FormData):
                     value = received_body.getlist(field.alias)
                 else:
                     value = received_body.get(field.alias)


Test patch:
diff --git a/tests/test_forms_from_non_typing_sequences.py b/tests/test_forms_from_non_typing_sequences.py
new file mode 100644
--- /dev/null
+++ b/tests/test_forms_from_non_typing_sequences.py
@@ -0,0 +1,46 @@
+from fastapi import FastAPI, Form
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+@app.post("/form/python-list")
+def post_form_param_list(items: list = Form(...)):
+    return items
+
+
+@app.post("/form/python-set")
+def post_form_param_set(items: set = Form(...)):
+    return items
+
+
+@app.post("/form/python-tuple")
+def post_form_param_tuple(items: tuple = Form(...)):
+    return items
+
+
+client = TestClient(app)
+
+
+def test_python_list_param_as_form():
+    response = client.post(
+        "/form/python-list", data={"items": ["first", "second", "third"]}
+    )
+    assert response.status_code == 200
+    assert response.json() == ["first", "second", "third"]
+
+
+def test_python_set_param_as_form():
+    response = client.post(
+        "/form/python-set", data={"items": ["first", "second", "third"]}
+    )
+    assert response.status_code == 200
+    assert set(response.json()) == {"first", "second", "third"}
+
+
+def test_python_tuple_param_as_form():
+    response = client.post(
+        "/form/python-tuple", data={"items": ["first", "second", "third"]}
+    )
+    assert response.status_code == 200
+    assert response.json() == ["first", "second", "third"]