2

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.

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

2 Comments

From the mypy documentation: "Literal types may contain one or more literal bools, ints, strs, bytes, and enum values. However, literal types cannot contain arbitrary expressions: types like Literal[my_string.trim()], Literal[x > 3], or Literal[3j + 4] are all illegal." So Literal[values] is valid python syntax, but will not be understood by any type checker, which completely defies the point of adding type hints in the first place. mypy.readthedocs.io/en/stable/literal_types.html#limitations
Yes, thanks for the comment! When using Pydantic one has to keep in mind that there are specific patterns, which are not recognized by standard type checkers. However for people not using type checkers, but just type hints for Pydantic it is still relevant.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.