pythonpydantic

Creating a custom Pydantic Field to accept "str" or "None" Values


I want to create a Pydantic custom field. The main goal of this validator is to be able to accept two data types: "str" and "None". If the value is "None", it should return an empty string. I tried to do it as follows:

from pydantic import BaseModel


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

    @classmethod
    def validate(cls, v):
        if v is None:
            return ""
        return str(v)


class Model(BaseModel):
    url: EmptyStringField


model = Model(url=None)
print(model.url)

However, I'm getting the following error:

url
  none is not an allowed value (type=type_error.none.not_allowed)

Solution

  • If all you want is for the url field to accept None as a special case, but save an empty string instead, you should still declare it as a regular str type field. You can handle the special case in a custom pre=True validator. No need for a custom data type there.

    from pydantic import BaseModel, validator
    
    
    class Model(BaseModel):
        url: str
    
        @validator("url", pre=True)
        def none_to_empty(cls, v: object) -> object:
            if v is None:
                return ""
            return v
    
    
    model = Model(url=None)
    print(model.json())  # {"url": ""}
    

    Update

    If you don't want to repeat the validator in different models for different fields, you can define a catch-all pre=True validator on a custom base model and implement some sort of logic to discern, which fields on the model to process and how.

    One option is to utilize typing.Annotated to "package" any given type with some custom conversion function that should be called. The catch-all validator would then check every field for Annotated metadata and if it finds a function, it would call that on the value.

    Here is a working example:

    from typing import Annotated, get_origin
    
    from pydantic import BaseModel as PydanticBaseModel, validator
    from pydantic.fields import ModelField
    
    
    class BaseModel(PydanticBaseModel):
        @validator("*", pre=True)
        def process_annotated(cls, v: object, field: ModelField) -> object:
            if get_origin(field.annotation) is not Annotated:
                return v
            func = field.annotation.__metadata__[0]
            if not callable(func):
                return v
            return func(v)
    
    
    StrOrNone = Annotated[str, lambda v: "" if v is None else v]
    
    
    class Model1(BaseModel):
        url: StrOrNone
    
    
    print(Model1(url=None).json())  # {"url": ""}