GopherM
GopherM

Reputation: 700

pydantic validate all Literal fields

I have multiple pydantic 2.x models and instead of applying validation per each literal field on each model

class MyModel(BaseModel):
    name: str = ""
    description: Optional[str] = None
    sex: Literal["male", "female"]

    @field_validator("sex", mode="before")
    @classmethod
    def strip_sex(cls, v: Any, info: ValidationInfo):
        if isinstance(v, str):
            return v.strip()
        return v

I want to use approach similar to this Annotated Validators

How can I achieve automatic validation on all Literal fields?

def strip_literals(v: Any) -> Any:
    if isinstance(v, str):
        return v.strip()
    return v

# doesn't work
# LiteralType = TypeVar("LiteralType", bound=Literal)
# LiteralStripped = Annotated[Literal, BeforeValidator(strip_literals)]

class MyModel(BaseModel):
    name: str = ""
    description: Optional[str] = None
    sex: LiteralStripped["male", "female"]

I want something like above, but cannot actually define proper validation handlers on literals.

Upvotes: 2

Views: 757

Answers (1)

Axel Donath
Axel Donath

Reputation: 1638

You have to move the declaration of the Literal values into the annotation, like so:

from typing import Any, Optional, Annotated, TypeVar, Literal
from pydantic import BaseModel, BeforeValidator

def strip_literals(v: Any) -> Any:
    if isinstance(v, str):
        return v.strip()
    return v

LiteralStripped = Annotated[Literal["male", "female"], BeforeValidator(strip_literals)]

class MyModel(BaseModel):
    name: str = ""
    description: Optional[str] = None
    sex: LiteralStripped

m = MyModel(sex="foo")

Which raises:

ValidationError: 1 validation error for MyModel
sex
  Input should be 'male' or 'female' [type=literal_error, input_value='foo', input_type=str]
    For further information visit https://errors.pydantic.dev/2.5/v/literal_error

In case you would like to avoid the repetition of the annotation, for different Literal values you can define an alias and rely on __class_getitem__:

from typing import Any, Optional, Annotated, TypeVar, Literal
from pydantic import BaseModel, BeforeValidator


class LiteralStripped:
    @staticmethod
    def strip_literals(v: Any) -> Any:
        if isinstance(v, str):
            return v.strip()
        return v

    def __class_getitem__(cls, values):

        return Annotated[Literal[values], BeforeValidator(cls.strip_literals)]

class MyModel(BaseModel):
    name: str = ""
    description: Optional[str] = None
    sex: LiteralStripped["male", "female"]

m = MyModel(sex="foo")

However this is already an advanced pattern, but it works exactly as you suggested.

I hope this helps.

Upvotes: 3

Related Questions