Search code examples
pythontype-conversionpydanticpydantic-v2

How to make Pydantic's non-strict, coercive mode apply to integer literals?


I'm validating inputs to a function using Pydantic's @validate_call as follows:

from typing import Literal
from pydantic import validate_call

@validate_call
def foo(a: Literal[0, 90, 180, 270]) -> None:
    print(a, type(a))

I want Pydantic to perform its default type coercion like it does with the int type:

foo(90)    # Works as expected
foo('90')  # Doesn't work, but I want it to

If I use the annotation a: int, it will coerce strings like '180', but then I have to manually validate which integers are given.

How do I make Pydantic perform type coercion on Literals?

Note: I'll accept a solution that requires a to be a string type instead of an integer, as long as it still allows both integer and string input.


Bad Solutions

  • I don't want to add every literal case. Literal[0, 90, 180, 270, '0', '90', '180', '270'] is bad because it doesn't allow the strings '-0' or '180.0'.

  • I could do Annotated[int, Field(ge=0, le=0)] | Annotated[int, Field(ge=90, le=90)] | ..., but that's stupidly verbose.

  • I don't want to define some separate function or model. At that point, it's easier to just accept a: int and validate the particular value inside the method.


Solution

  • You can combine the BeforeValidator and the Literal like this:

    from typing import Annotated, Literal
    from pydantic import validate_call, BeforeValidator, ValidationError
    
    # First try has the following validator:
    # BeforeValidator(int)
    
    @validate_call
    def foo(a: Annotated[Literal[0, 90, 180, 270], BeforeValidator(float)]) -> None:
        print(a, type(a))
    
    
    if __name__ == "__main__":
        foo("90")
        foo("180.0")
        foo("180.0")
        try:
            foo(0.1)
        except ValidationError as err:
            print(err)
        try:
            foo("70")
        except ValidationError as err:
            print(err)
        try:
            foo("can't convert to int")
        except ValueError as err:
            print(err)
    

    The BeforeValidator function will be called before checks and thus the literal validation will be done against an integer.

    Edit: better manage string with decimal number.