9

Note: This question is different from the one here, in that I need it to work with Swagger.

Given a FastAPI GET endpoint, I want to allow any arbitrary set of URL parameters, while maintaining Swagger support.

My use case is that I want to support a JSON API-like set of query parameters such as this:

/api/books/?include=author&sort=name,zip&sort[author]=-lname&fields=name,phone,street

The use of square brackets prevents me from using traditional classes to model query parameters, so I'm directly using the Request object instead. However, I would like to use Swagger to test the endpoint. I can't find a way to provide arbitrary URL parameters. I'm happy to type them in as a single string.

One would think something like the following:

def books(**params): 
    ....

That gives a curl statement of:

api/books?params=sort%5Bone%5D%3Dtwo'

what I really want is:

api/books?sort&one%5D%3Dtwo'
Chris
  • 18,724
  • 6
  • 46
  • 80
SteveJ
  • 3,034
  • 2
  • 27
  • 47

1 Answers1

2

You could use an Optional string parameter (book_params in the case below) to pass the query parameters as a single string through OpenAPI (Swagger UI) e.g., include=author&sort=name,zip&sort[author]=-lname&fields=name,phone,street. You can then parse the query data (using urllib.parse.parse_qs) to get a dictionary, as shown below.

The below example also utilises the method described here, in order to fix the part where parse_qs parses single values into lists (e.g., 'foo=bar' would be parsed into foo = ['bar']), while also preserving all the values for keys that the user passes a list. For example, if the user passed the same key multiple times in the URL, that is, for instance, 'foo=2&bar=7&foo=10', using dict(request.query_params) to retrieve the query parameters would result in {"foo":"10","bar":"7"} instead of {"foo":["2","10"],"bar":"7"}. The approach, however, demonstrated in the example below (using the aforementioned method) takes care of that as well, by parsing the query string (which can be retieved using request.url.query) and makes sure that actual lists are preserved.

You can check whether this optional parameter, i.e., book_params, is empty or not to decide whether to read the query params using book_params (meaning that the request is sent through Swagger) or using the Request object directly (meaning that the request is sent through typing the URL into the address bar of the browser e.g., http://127.0.0.1:8000/api/books?include=author&sort=name,zip&sort[author]=-lname&fields=name,phone,street, or using some other client app). Please make sure to name that optional parameter (i.e., book_params) something unique, which wouldn't also be part of the actual parameters.

from fastapi import FastAPI, Request
from typing import Optional
from urllib.parse import parse_qs

app = FastAPI()

@app.get("/api/books")
def books(request: Request, book_params: Optional[str] = None):
    q_params = {}
    
    if book_params is not None:
        q_params = parse_qs(book_params, keep_blank_values=True)
    else:   
        q_params = parse_qs(request.url.query, keep_blank_values=True)
        
    d = dict((k, v if len(v)>1 else v[0]) 
                for k, v in q_params.items())

    return d
Chris
  • 18,724
  • 6
  • 46
  • 80
  • Interesting, Chris --thanks for the input. I'll give this some thought, it isn't an exact match but may be close enough to be usable. – SteveJ Jan 25 '22 at 22:26