Fastapi: [QUESTION] How to pass list in query parameters?

Created on 22 Feb 2019  路  9Comments  路  Source: tiangolo/fastapi

Description

I need to support several query parameters with same name in GET route. Typical request looks like this:
http://localhost/item?num=1&num=2

I configured a route
@app.get("/item", content_type=UJSONResponse) async def get_part(num: list): found = False for i in num: if i in some_data: found = True break return {"found": found}
The idea is, that I can pass several numbers to check, if they exist in some_data. In my opinion, it is not a good idea to get numbers from body here, because it is a simple GET request, but in current version application expects argument "num" in body, so on my request (written above) I receive a response

{"detail":[{"loc":["body","num"],"msg":"field required","type":"value_error.missing"}]}

As I know, it is not restricted to pass several query parameters with the same name in an HTTP request by any specifications, so I would like to ask, is it possible to configure a route in FastAPI, which will be able to parse several query parameters with the same name?

question

Most helpful comment

It is supported (and tested), but not documented yet, but you can just use standard Python types (a List) :)

from typing import List

from fastapi import FastAPI, Query

app = FastAPI()
@app.get("/items/")
def read_items(q: List[int] = Query(None)):
    return {"q": q}

All 9 comments

It is supported (and tested), but not documented yet, but you can just use standard Python types (a List) :)

from typing import List

from fastapi import FastAPI, Query

app = FastAPI()
@app.get("/items/")
def read_items(q: List[int] = Query(None)):
    return {"q": q}

It is now properly documented :memo: :tada:

For query parameters: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#query-parameter-list-multiple-values

And for duplicate headers: https://fastapi.tiangolo.com/tutorial/header-params/#duplicate-headers

I guess this is solved now, so I'll close the issue. But feel free to add more comments or create new issues.

Is it possible to do:
http://localhost/item?num=1,2,3,4,5,6

instead of:
http://localhost/item?num=1&num=2&num=3&num=4&num=5&num=6?

@neilpanchal You could do this by adding a custom dependency that expected a string-value query parameter, then splits the string on , and converts the result to ints. But obviously that's a little less ergonomic.

I think the main reason why this isn't easier is that FastAPI is basically just deferring to starlette for query parameter string extraction, then pydantic for parsing.

So starlette gives query parameter num="1,2,3,4,5,6", and pydantic doesn't know how to parse "1,2,3,4,5,6" into a list of ints.

Starlette does have built-in support for repeated values of the same query parameter, which is why the more verbose approach does work out of the box.

here's an example dependency that I wrote to handle this sort of thing

I wanted to be able to handle normal query arg lists as well as these awful "string" lists that are wrapped in square brackets and sometimes contain double quotes and spaces that were somehow a part of our existing API.

def parse_list(names: List[str] = Query(None)) -> Optional[List]:
    """
    accepts strings formatted as lists with square brackets
    names can be in the format
    "[bob,jeff,greg]" or '["bob","jeff","greg"]'
    """
    def remove_prefix(text: str, prefix: str):
        return text[text.startswith(prefix) and len(prefix):]

    def remove_postfix(text: str, postfix: str):
        if text.endswith(postfix):
            text = text[:-len(postfix)]
        return text

    if names is None:
        return

    # we already have a list, we can return
    if len(names) > 1:
        return names

    # if we don't start with a "[" and end with "]" it's just a normal entry
    flat_names = names[0]
    if not flat_names.startswith("[") and not flat_names.endswith("]"):
        return names

    flat_names = remove_prefix(flat_names, "[")
    flat_names = remove_postfix(flat_names, "]")

    names_list = flat_names.split(",")
    names_list = [remove_prefix(n.strip(), "\"") for n in names_list]
    names_list = [remove_postfix(n.strip(), "\"") for n in names_list]

    return names_list


@app.get("/hello_list")
def hello_list(names: List[str] = Depends(parse_list)):
    """ list param method """

    if names is not None:
        return StreamingResponse((f"Hello {name}" for name in names))
    else:
        return {"message": "no names"}

Hey @falkben (or any one else in the thread),

This is a great solution and works great.

But I had to refactor my method signature from:

@app.get("/hello_list")
def hello_list(names: List[str] = Query(None, description="List of names to greet")):
    """ list param method """

to

@app.get("/hello_list")
def hello_list(names: List[str] = Depends(parse_list)):
    """ list param method """

and I lost the nice documentation provided by the 'description' field.

Any idea how I could combine Query with Depends? Should I open another issue for greater visibility?

You should be able to move the description into the Depends method. So in the example, the depends method signature would look like this:

def parse_list(names: List[str] = Query(None, description="List of names to greet")) -> Optional[List]:

Hmmm, that is interesting, but that locks you into using the same description to all query params that you might want to check.

But wait, now that I am looking at it, this looks like it is tied to one query param name only, and the same 'parse_list` method cannot be reused for another param....? :(

Seems like, I'll have to take a closer look...

[EDITED]: to say that your suggestion is indeed working, for one param that appears as a list, but I cannot add a second one.

Was this page helpful?
0 / 5 - 0 ratings