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)
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": ""}
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": ""}