pythonpydantic

Pydantic custom datatype doesn't take values when validating


I have the following Pydantic v1.10 custom datatype:

class SelectionList(ABC):
    class_selection_list = []
    datatype = None

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def __modify_schema__(cls, field_schema):
        field_schema.update(selection_list=cls.class_selection_list, type=cls.datatype)

    @classmethod
    def validate(cls, v):
        if v not in cls.class_selection_list:
            raise Exception(f"Value must be on of this list: {str(cls.class_selection_list)}")


class StatusType(str, SelectionList):
    class_selection_list = ["Active", "Inactive"]
    datatype = "string"

And the following model:

class MyModel(BaseModel):
    ID: int = Field(alias="Id", frozen=True)
    STATUS: StatusType = Field(alias="Status")

The MyModel.schema() works find but, when trying to validate a record using modeled_record = MyModel(**record), the field value, which is a valid value, is not taken and the field Status of the model is None.

What Am I missing?


Solution

  • What Am I missing?

    Your validator is not returning a value, which is why the field value is None. You need:

    class SelectionList(ABC):
        class_selection_list = []
        datatype = None
    
        @classmethod
        def __get_validators__(cls):
            yield cls.validate
    
        @classmethod
        def __modify_schema__(cls, field_schema):
            breakpoint()
            field_schema.update(selection_list=cls.class_selection_list, type=cls.datatype)
    
        @classmethod
        def validate(cls, v):
            if v not in cls.class_selection_list:
                raise Exception(
                    f"Value must be on of this list: {str(cls.class_selection_list)}"
                )
    
            return cls(v)
    

    Note that we're returning cls(v) instead of simply v; the latter would make the value of the Status field a str, while returning cls(v) makes the value of Status a StatusType.


    For what you're doing in this example, it would be much simpler to use enum.StrEnum:

    from pydantic import BaseModel, Field
    from enum import StrEnum
    
    class StatusType(StrEnum):
        ACTIVE = "Active"
        INACTIVE = "Inactive"
    
    
    class MyModel(BaseModel):
        ID: int = Field(alias="Id", frozen=True)
        STATUS: StatusType = Field(alias="Status")
    

    That gets you the same behavior:

    >>> MyModel(Id=1, Status="invalid")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
    pydantic.error_wrappers.ValidationError: 1 validation error for MyModel
    Status
      value is not a valid enumeration member; permitted: 'Active', 'Inactive' (type=type_error.enum; enum_values=[<StatusType.ACTIVE: 'Active'>, <StatusType.INACTIVE: 'Inactive'>])
    >>> MyModel(Id=1, Status="Active")
    MyModel(ID=1, STATUS=<StatusType.ACTIVE: 'Active'>)
    >>> x=MyModel(Id=1, Status="Active")
    >>> x.json()
    '{"ID": 1, "STATUS": "Active"}'
    

    And just because I mentioned it in a comment, here's a solution using Pydantic 2.x annotated validators (but note that it still makes more sense to use the StrEnum solution instead):

    from typing import Annotated
    from pydantic import BaseModel, Field, AfterValidator
    
    
    def stringInList(*valid_values):
        def _validate(v):
            if v not in valid_values:
                raise ValueError(f"value must be one of {', '.join(valid_values)}")
            return v
    
        return _validate
    
    
    StatusType = Annotated[str, AfterValidator(stringInList("Active", "Inactive"))]
    
    
    class MyModel(BaseModel):
        ID: int = Field(alias="Id", frozen=True)
        STATUS: StatusType = Field(alias="Status")