konradhalas / dacite

Simple creation of data classes from dictionaries.
MIT License
1.72k stars 106 forks source link

Coerce `None` values to default values. #186

Closed elpescado closed 1 year ago

elpescado commented 2 years ago

Consider:

from dataclasses import dataclass

@dataclass
class Widget:
    val: str = "test"

x = from_dict(data_class=Widget, data={}). # No `val` at all, default value is used
print(x)
y = from_dict(data_class=Widget, data={"val": None})
print(y)
Widget(val='test')
Traceback (most recent call last):
  File "dac.py", line 10, in <module>
    y = from_dict(data_class=Widget, data={"val": None})
  File "/lib/python3.9/site-packages/dacite/core.py", line 68, in from_dict
    raise WrongTypeError(field_path=field.name, field_type=field.type, value=value)
dacite.exceptions.WrongTypeError: wrong value type for field "val" - should be "str" instead of value "None" of type "NoneType"

When a field has a default value and is not declared as Optional, it could be useful to have (possibly an opt-in config option) to use default values instead of None. Currently the workaround is to use __post_init__ method of a dataclass:

@dataclass
class Widget:
    val: Optional[str] = "test"

    def __post_init__(self):
        if self.val is None:
            self.val = "test"

Which is, IMO, not elegant, requires field to be declared as Optional and requires repeated default value declarations.

hawkiboy commented 1 year ago

Agree, I don't want a field to be optional, but I have a huge noSQL db to coerce into dataclass. Fields are optional in MongoDB of course, but if instead it is set to null, dacite crashes out.

konradhalas commented 1 year ago

@elpescado thank you for reporting this issue.

TBH I think that the current behaviour is correct: if field can have None value, it should be Optional. You can always remove all your fields with value None before you pass data to from_dict - dacite will assign default values for such field.

Example:

from dataclasses import dataclass

from dacite import from_dict

@dataclass
class Widget:
    val: str = "test"

data = {"val": None}
x = from_dict(data_class=Widget, data={k: v for k, v in data.items() if v is not None}) 
print(x)  # Widget(val='test')