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.
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.
1 Answer
Reset to default 2You 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.
int(a) in (0, 90, 180, 270)
would do it. – Klaus D. Commented Feb 14 at 4:54@validate_call
to check all the parameter types, and lenient duck typing is an added bonus that works on all the other parameters. Normally it's less code as well, though admittedly not here. I suppose it's just personal preference – kviLL Commented Feb 15 at 16:51