Guy NANA
Guy NANA

Reputation: 113

FastAPI with Pydantic and Typing Literal throws literal_error (422 Unprocessable Entity)

I defined a custom pydantic class and a FastAPI route as below:

class QuestionParameters(BaseModel):
    test_type: Union[Literal["single_choice", "multiple_choices"], None] = None
    number_of_questions: Union[Literal[5, 10, 15], None] = None
    categories: Union[List[str], None] = None

@app.post("/generate_quiz")
def generate_quiz(qcm_params: Annotated[QuestionParameters, Query()], user: str = Depends(verify_credentials)):
    """Génère un QCM basé sur les paramètres fournis.
    
    PARAMS:
    -------
    - test_type: str
        Le type de test souhaité. Par exemple "multiple_choices"
    - categories: List[str]
        Une liste des catégories de questions souhaitées.
    - number_of_questions: int
        Le nombre de question à inclure dans le QCM

    RETURN:
    -------
    List[Question]
    
    """
    test_type = qcm_params.test_type
    number_of_questions = qcm_params.number_of_questions
    categories = qcm_params.categories
    ....

When I do a this request:

curl -X 'POST' \
  'http://localhost:8000/generate_quiz?test_type=single_choice&number_of_questions=10&categories=Automation&categories=Data%20Science' \
  -H 'accept: application/json'

I get the following error message:

{
  "detail": [
    {
      "type": "literal_error",
      "loc": [
        "query",
        "number_of_questions"
      ],
      "msg": "Input should be 5, 10 or 15",
      "input": "10",
      "ctx": {
        "expected": "5, 10 or 15"
      }
    }
  ]
}

I don't understand why the query parameter number_of_questions is considered as string input besides it's an int value (10) I past as shown in curl request or FastAPI docs. enter image description here

Could someone explain me what is wrong ? Thanks!

Upvotes: 3

Views: 68

Answers (1)

Anerdw
Anerdw

Reputation: 1935

This is a known issue. One user suggested this workaround using an annotation and a BeforeValidator to convert passed strings into ints before validation. In your case, it would look something like this:

class QuestionParameters(BaseModel):
    test_type: Union[Literal["single_choice", "multiple_choices"], None] = None
    number_of_questions: Union[Annotated[Literal[5, 10, 15], BeforeValidator(int)], None] = None
    categories: Union[List[str], None] = None

This is obviously a little verbose. If Python 3.10 or later is an option, you can use the cleaner syntax for unions (as well as the 3.9+ syntax for lists) to cut it down a little:

class QuestionParameters(BaseModel):
    test_type: Literal["single_choice", "multiple_choices"] | None = None
    number_of_questions: Annotated[Literal[5, 10, 15], BeforeValidator(int)] | None = None
    categories: list[str] | None = None

Here's a small test suite; it should work for either version.

>>> QuestionParameters(number_of_questions = None) # None
QuestionParameters(test_type=None, number_of_questions=None, categories=None)
>>> 
>>> QuestionParameters(number_of_questions = "5") # Acceptable "integer"
QuestionParameters(test_type=None, number_of_questions=5, categories=None)
>>> 
>>> QuestionParameters(number_of_questions = "0") # Unacceptable "integer"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/packages/pydantic/main.py", line 209, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for QuestionParameters
number_of_questions
  Input should be 5, 10 or 15 [type=literal_error, input_value=0, input_type=int]
    For further information visit https://errors.pydantic.dev/2.9/v/literal_error
>>> 
>>> QuestionParameters(number_of_questions = "Hello World!") # Non-numeric string
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/packages/pydantic/main.py", line 209, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for QuestionParameters
number_of_questions
  Value error, invalid literal for int() with base 10: 'Hello World!' [type=value_error, input_value='Hello World!', input_type=str]
    For further information visit https://errors.pydantic.dev/2.9/v/value_error
>>> 
>>> QuestionParameters(number_of_questions = "5.0") # "Float"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/packages/pydantic/main.py", line 209, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for QuestionParameters
number_of_questions
  Value error, invalid literal for int() with base 10: '5.0' [type=value_error, input_value='5.0', input_type=str]
    For further information visit https://errors.pydantic.dev/2.9/v/v

Upvotes: 1

Related Questions