6

Can I make a default value in pydantic if None is passed in the field without using validators?

I have the following code, but it seems to me that the validator here is superfluous for contract_ndfl. Is there any way to do without a validator?

My code:

  class User(BaseModel):
        user: int
        s_name: str
        contract_ndfl: Optional[int]
        

       @validator('contract_ndfl')
       def set_contract_ndfl(cls, v):
           return v or 13

Wishful code:

class User(BaseModel):
      user: int
      s_name: str
      contract_ndfl: Optional[int] = 13
            
Fyzzys
  • 756
  • 1
  • 7
  • 13
  • 1
    The collaborator on GitHub said that the desired goal can only be fulfilled by the code that is currently written. – Fyzzys Aug 20 '20 at 13:03

3 Answers3

12

Maybe you can use a validator for all field and define a BaseClass for it!

class NoneDefaultModel(BaseModel):

    @validator("*", pre=True)
    def not_none(cls, v, field):
        if all(
            (
                # Cater for the occasion where field.default in (0, False)
                getattr(field, "default", None) is not None,
                v is None,
            )
        ):
            return field.default
        else:
            return v

Then you can use a Subclass to Implement your wishful code:

class Bar(NoneDefaultModel):
    bar: int = 5

Bar(bar=None)
# Bar(bar=5)
alex li
  • 614
  • 8
  • 10
  • 2
    Note that this will apply the default even if the field is `Optional` and a `None` is given explicitly. The check inside the validator should also check for `field.allow_none` and only apply the default if it is `False`. – theberzi Jun 29 '22 at 08:47
  • 1
    @theberzi if the solution needs to coerce/modify a None to a field's default value, then field.allow_none should not be used. Also, field.allow_none cannot be set using the Field() construct. – theQuestionMan Jul 04 '22 at 08:19
  • You are right @theberzi. When you use something like "Union", "Optional[T, None]" or "None is given explicitly", then "field.allow_none" will be denoted as "True". If you want to "force" the default value if a "None" value is passed, don't use these "guys". So, be sure to check that "field.allow_none" is False. – Eduardo Lucio Dec 28 '22 at 19:59
0

Rather than using a validator, you can also overwrite __init__ so that the offending fields are immediately omitted:

class PreferDefaultsModel(BaseModel):
"""
Pydantic model that will use default values in place of an explicitly passed `None` value.
This is helpful when consuming APIs payloads which may explicitly define a field as `null`
rather than omitting it.
"""

def _field_allows_none(self, field_name):
    """
    Returns True if the field is exists in the model's __fields__ and it's allow_none property is True.
    Returns False otherwise.
    """
    field = self.__fields__.get(field_name)
    if field is None:
        return False
    return field.allow_none

def __init__(self, **data):
    """
    Removes any fields from the data which are None and are not allowed to be None.
    The results are then passed to the super class's init method.
    """
    data_without_null_fields = {k: v for k, v in data.items() if (
            v is not None
            or self._field_allows_none(k)
    )}
    super().__init__(**data_without_null_fields)

This can then be used in place of BaseModellike so:

class Foo(PreferDefaultsModel):
    automatic_field = 1
    explicit_field: int = Field(default=2)
    default_factory_field: int = Field(default_factory=lambda:3)
    optional_field: Optional[int] = Field(default=4)
    
f = Foo(automatic_field=None, explicit_field=None, default_factory_field=None, optional_field=None)    
print(f.json(indent=True))
{
 "explicit_field": 2,
 "default_factory_field": 3,
 "optional_field": null,
 "automatic_field": 1
}

Note that the optional field is not overwritten.

This approach will simply activate the fields' default behavior, no matter how it is defined. This is far less likely to produce unexpected results than using a validator.

Ian Burnette
  • 1,020
  • 10
  • 16
0

Answer based on the one by @alex li , but for Pydantic 2.X and with some improvements!

Test script (test.py)

from typing import Any

from pydantic import BaseModel, Field, FieldValidationInfo, field_validator
from pydantic_core import PydanticUndefined


class MyClass(BaseModel):
    my_field: int = Field(
        default=20,
        title="My field.",
        description="My field.",
        examples=["20"],
    )

    @field_validator("*", mode="before")
    @classmethod
    def use_default_value(cls, value: Any, info: FieldValidationInfo) -> Any:

        # NOTE: All fields that are optional for values, will assume the value in
        # "default" (if defined in "Field") if "None" is informed as "value". That
        # is, "None" is never assumed if passed as a "value".
        if (
            cls.model_fields[info.field_name].get_default() is not PydanticUndefined
            and not cls.model_fields[info.field_name].is_required()
            and value is None
        ):
            return cls.model_fields[info.field_name].get_default()
        else:
            return value


print(MyClass(my_field=30))
print(MyClass(my_field=None))
print(MyClass())

On BASH terminal

(my-python-script) [user@my-pc my-python-script]$ python test.py
my_field=30
my_field=20
my_field=20
starball
  • 20,030
  • 7
  • 43
  • 238
Eduardo Lucio
  • 1,771
  • 2
  • 25
  • 43