FastAPI swagger doesn't like list of strings passed via query parameter but endpoint works in browser
Asked Answered
C

1

5

I have a problem with a REST API endpoint in FastAPI that accepts a list of strings via a single query parameter. An example of this endpoint's usage is:

http://127.0.0.1:8000/items/2?short=false&response=this&response=that

Here, the parameter named 'response' accepts a list of strings as documented in FastAPI tutorial, section on Query Parameters and String Validation. The endpoint works as expected in the browser.

Fig. 1

However, it does not work in Swagger docs. The button labeled 'Add string item' shakes upon clicking 'Execute' to test the endpoint. Swagger UI seems unable to create the expected URL with the embedded query parameters (as shown in Fig 1.).

Fig. 2

The code for the endpoint is as follows. I have tried with and without validation.

@app.get("/items/{item_ID}")
async def getQuestion_byID(item_ID: int = Path(
                    ...,
                    title = "Numeric ID of the question",
                    description = "Specify a number between 1 and 999",
                    ge = 1,
                    le = 999
                ), response: Optional[List[str]] = Query(
                    [],
                    title="Furnish an answer",
                    description="Answer can only have letters of the alphabet and is case-insensitive",
                    min_length=3,
                    max_length=99,
                    regex="^[a-zA-Z]+$"
                ), short: bool = Query(
                    False,
                    title="Set flag for short result",
                    description="Acceptable values are 1, True, true, on, yes"
                )):
    """
    Returns the quiz question or the result.
    Accepts item ID as path parameter and
    optionally response as query parameter.
    Returns result when the response is passed with the item ID. 
    Otherwise, returns the quiz question.
    """
    item = question_bank.get(item_ID, None)
    if not item:
        return {"question": None}
    if response:
        return evaluate_response(item_ID, response, short)
    else:
        return {"question": item["question"]}

Grateful for any help.

Comatulid answered 18/3, 2022 at 19:8 Comment(3)
1) If you hover over the "Add string item" button when it's red, what does the tooltip say? 2) Can you export the OpenAPI definition from Swagger UI and post the portion with the response parameter? I suspect that the FastAPI code annotations are slightly incorrect.Antoine
Possibly related: github.com/tiangolo/fastapi/issues/4345. See also github.com/tiangolo/fastapi/issues/1021#issuecomment-590353181Antoine
@Antoine hello! When I hover over the button, the tooltip says "Value must follow pattern..". But the pattern-matching works as expected when I invoke the endpoint through browser.Comatulid
T
6

As described here, this happens due to that OpenAPI applies the pattern (as well as minimum and maximum constraints) to the schema of the array itself, not just the individual items in the array. If you checked the OpenAPI schema at http://127.0.0.1:8000/openapi.json, you would see that the schema for the response parameter appears as shown below (i.e., validations are being applied to the array itself as well):

      {
        "description": "Answer can only have letters of the alphabet and is case-insensitive",
        "required": false,
        "schema": {
          "title": "Furnish an answer",
          "maxLength": 99,
          "minLength": 3,
          "pattern": "^[a-zA-Z]+$",
          "type": "array",
          "items": {
            "maxLength": 99,
            "minLength": 3,
            "pattern": "^[a-zA-Z]+$",
            "type": "string"
          },
          "description": "Answer can only have letters of the alphabet and is case-insensitive",
          "default": []
        },
        "name": "response",
        "in": "query"
      }

Solution 1

As mentioned here, you could use a Pydantic constr instead to specify items with that contraint:

my_constr = constr(regex="^[a-zA-Z]+$", min_length=3, max_length=99)
response: Optional[List[my_constr]] = Query([], title="Furnish an...", description="Answer can...")

Solution 2

Keep your response parameter as is. Copy the OpenAPI schema from http://127.0.0.1:8000/openapi.json, remove the pattern (as well as minimum and maximum attributes) from response's (array) schema and save the OpenAPI schema to a new file (e.g., my_openapi.json). It should look like this:

    ...
    {
    "description": "Answer can only have letters of the alphabet and is case-insensitive",
    "required": false,
    "schema": {
      "title": "Furnish an answer",
      "type": "array",
      "items": {
        "maxLength": 99,
        "minLength": 3,
        "pattern": "^[a-zA-Z]+$",
        "type": "string"
      },
      "description": "Answer can only have letters of the alphabet and is case-insensitive",
      "default": []
    },
    "name": "response",
    "in": "query"
    },
    ...

Then, in your app, instruct FastAPI to use that schema instead:

import json
app.openapi_schema = json.load(open("my_openapi.json"))

Solution 3

Since the above solution would require you to copy and edit the schema every time you make a change or add new endpoints/parameters, you would rather modify the OpenAPI schema as described here. This would save you from copying/editing the schema file. Make sure to add the below at the end of your code (after defining all the routes).

from fastapi.openapi.utils import get_openapi

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="FastAPI",
        version="0.1.0",
        description="This is a very custom OpenAPI schema",
        routes=app.routes,
    )
    del openapi_schema["paths"]["/items/{item_ID}"]["get"]["parameters"][1]["schema"]["maxLength"]
    del openapi_schema["paths"]["/items/{item_ID}"]["get"]["parameters"][1]["schema"]["minLength"]
    del openapi_schema["paths"]["/items/{item_ID}"]["get"]["parameters"][1]["schema"]["pattern"]
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema
    
    
app.openapi = custom_openapi

In all the above solutions, the constraints annotation that would normally be shown in OpenAPI under response (i.e., (query) maxLength: 99 minLength: 3 pattern: ^[a-zA-Z]+$), won't appear (since Swagger would create that annotation from the constraints applied to the array, not the items), but there doesn't seem to be a way to preserve that. In Solutions 2 and 3, however, you could modify the "in" attribute, shown in the JSON code snippet above, to manually ddd the annotation. But, as HTML elements, etc., are controlled by Swagger, the whole annotation would appear inside parentheses and without line breaks between the constraints. Nevertheless, you could still inform users about the constraints applied to items, by specifying them in the description of your Query parameter.

Treasatreason answered 19/3, 2022 at 9:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.