Repository: yukinarit/pyserde
Pull request #191 (2022-01-22 13:22:35)

Fix Optional in custom class deserializer
This serde Foo class works as expected.

```python
def fallback(_, __):
    raise SerdeSkip()

@serde(serializer=fallback, deserializer=fallback)
class Foo:
    a: Optional[str]
```



Patch:
diff --git a/docs/features/custom-field-serializer.md b/docs/features/custom-field-serializer.md
index d13e91e..847dd03 100644
--- a/docs/features/custom-field-serializer.md
+++ b/docs/features/custom-field-serializer.md
@@ -1,16 +1,15 @@
 # Custom field (de)serializer
 
-If you want to provide a custom function to override the default (de)serialization behaviour of a field, you can pass your functions to `serde_serializer` and `serde_deserializer` dataclass metadata.
+If you want to provide a custom function to override the default (de)serialization behaviour of a field, you can pass your functions to `serializer` and `deserializer` in `serde.field`.
 
 ```python
+from serde import serde, field
+
 @serde
 class Foo:
     dt1: datetime
     dt2: datetime = field(
-        metadata={
-            'serde_serializer': lambda x: x.strftime('%d/%m/%y'),
-            'serde_deserializer': lambda x: datetime.strptime(x, '%d/%m/%y'),
-        }
+        serializer=lambda x: x.strftime('%d/%m/%y'), deserializer=lambda x: datetime.strptime(x, '%d/%m/%y')
     )
 ```
 `dt1` in the example will serialized into `2021-01-01T00:00:00` because the pyserde's default (de)serializier for datetime is ISO 8601. `dt2` field in the example will be serialized into `01/01/21` by the custom field serializer.
diff --git a/serde/core.py b/serde/core.py
index b61a838..e7f9ee9 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -19,6 +19,8 @@ import stringcase
 from .compat import (
     SerdeError,
     dataclass_fields,
+    has_default,
+    has_default_factory,
     is_bare_dict,
     is_bare_list,
     is_bare_set,
@@ -485,6 +487,9 @@ class Field:
         """
         return conv(self, case or self.case)
 
+    def supports_default(self) -> bool:
+        return not getattr(self, "iterbased", False) and (has_default(self) or has_default_factory(self))
+
 
 F = TypeVar('F', bound=Field)
 
diff --git a/serde/de.py b/serde/de.py
index a59fd17..8f22b6e 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -426,6 +426,12 @@ class DeField(Field):
     def data(self, d):
         self.datavar = d
 
+    def data_or(self):
+        if self.iterbased:
+            return self.data
+        else:
+            return f'{self.datavar}.get("{self.conv_name()}")'
+
 
 @dataclass
 class InnerField(DeField):
@@ -491,19 +497,11 @@ class Renderer:
         else:
             return f"raise_unsupported_type({arg.data})"
 
-        if not arg.iterbased and (has_default(arg) or has_default_factory(arg)):
-            exists = f'"{arg.conv_name()}" in {arg.datavar}'
-            if has_default(arg):
-                res = f'({res}) if {exists} else serde_scope.defaults["{arg.name}"]'
-            elif has_default_factory(arg):
-                res = f'({res}) if {exists} else serde_scope.defaults["{arg.name}"]()'
+        if arg.supports_default():
+            res = self.default(arg, res)
 
         if self.custom and not arg.deserializer:
-            # The function takes a closure in order to execute the default value lazily.
-            return (
-                f'serde_custom_class_deserializer({typename(arg.type)}, {arg.datavar}, {arg.data}, '
-                f'default=lambda: {res})'
-            )
+            return self.custom_class_deserializer(arg, res)
         else:
             return res
 
@@ -514,6 +512,16 @@ class Renderer:
         assert arg.deserializer
         return f"{arg.deserializer.name}({arg.data})"
 
+    def custom_class_deserializer(self, arg: DeField, code: str) -> str:
+        """
+        Render custom class deserializer.
+        """
+        # The function takes a closure in order to execute the default value lazily.
+        return (
+            f'serde_custom_class_deserializer({typename(arg.type)}, {arg.datavar}, {arg.data_or()}, '
+            f'default=lambda: {code})'
+        )
+
     def dataclass(self, arg: DeField) -> str:
         if not arg.flatten:
             # e.g. "data['field']" will be used as variable name.
@@ -669,6 +677,15 @@ Foo.__serde__.funcs['foo'](data[0][3], reuse_instances=reuse_instances),)"
         func_name = union_func_name(UNION_DE_PREFIX, type_args(arg.type))
         return f"serde_scope.funcs['{func_name}']({arg.data}, reuse_instances)"
 
+    def default(self, arg: DeField, code: str) -> str:
+        exists = f'"{arg.conv_name()}" in {arg.datavar}'
+        if has_default(arg):
+            return f'({code}) if {exists} else serde_scope.defaults["{arg.name}"]'
+        elif has_default_factory(arg):
+            return f'({code}) if {exists} else serde_scope.defaults["{arg.name}"]()'
+        else:
+            return code
+
 
 def to_arg(f: DeField, index, rename_all: Optional[str] = None) -> DeField:
     f.index = index


Test patch:
diff --git a/tests/test_custom.py b/tests/test_custom.py
index ddf5efa..3bbfa56 100644
--- a/tests/test_custom.py
+++ b/tests/test_custom.py
@@ -13,11 +13,11 @@ from serde.json import from_json, to_json
 def test_custom_field_serializer():
     @serde
     class Foo:
-        dt1: datetime
-        dt2: datetime = field(
+        a: datetime
+        b: datetime = field(
             serializer=lambda x: x.strftime('%d/%m/%y'), deserializer=lambda x: datetime.strptime(x, '%d/%m/%y')
         )
-        dt3: Optional[datetime] = field(
+        c: Optional[datetime] = field(
             serializer=lambda x: x.strftime('%d/%m/%y') if x else None,
             deserializer=lambda x: datetime.strptime(x, '%d/%m/%y') if x else None,
         )
@@ -25,7 +25,7 @@ def test_custom_field_serializer():
     dt = datetime(2021, 1, 1, 0, 0, 0)
     f = Foo(dt, dt, None)
 
-    assert to_json(f) == '{"dt1": "2021-01-01T00:00:00", "dt2": "01/01/21", "dt3": null}'
+    assert to_json(f) == '{"a": "2021-01-01T00:00:00", "b": "01/01/21", "c": null}'
     assert f == from_json(Foo, to_json(f))
 
     assert to_tuple(f) == (datetime(2021, 1, 1, 0, 0), '01/01/21', None)
@@ -76,21 +76,38 @@ def test_custom_class_serializer():
 
     @serde(serializer=serializer, deserializer=deserializer)
     class Foo:
-        i: int
-        dt1: datetime
-        dt2: datetime
-        s: Optional[str] = None
-        u: Union[str, int] = 10
+        a: int
+        b: datetime
+        c: datetime
+        d: Optional[str] = None
+        e: Union[str, int] = 10
 
     dt = datetime(2021, 1, 1, 0, 0, 0)
     f = Foo(10, dt, dt)
 
-    assert to_json(f) == '{"i": 10, "dt1": "01/01/21", "dt2": "01/01/21", "s": null, "u": 10}'
+    assert to_json(f) == '{"a": 10, "b": "01/01/21", "c": "01/01/21", "d": null, "e": 10}'
     assert f == from_json(Foo, to_json(f))
 
     assert to_tuple(f) == (10, '01/01/21', '01/01/21', None, 10)
     assert f == from_tuple(Foo, to_tuple(f))
 
+    def fallback(_, __):
+        raise SerdeSkip()
+
+    @serde(serializer=fallback, deserializer=fallback)
+    class Foo:
+        a: Optional[str]
+        b: str
+
+    f = Foo("foo", "bar")
+    assert to_json(f) == '{"a": "foo", "b": "bar"}'
+    assert f == from_json(Foo, '{"a": "foo", "b": "bar"}')
+    assert Foo(None, "bar") == from_json(Foo, '{"b": "bar"}')
+    with pytest.raises(Exception):
+        assert Foo(None, "bar") == from_json(Foo, '{}')
+    with pytest.raises(Exception):
+        assert Foo("foo", "bar") == from_json(Foo, '{"a": "foo"}')
+
 
 def test_field_serialize_override_class_serializer():
     def serializer(cls, o):
@@ -107,16 +124,16 @@ def test_field_serialize_override_class_serializer():
 
     @serde(serializer=serializer, deserializer=deserializer)
     class Foo:
-        i: int
-        dt1: datetime
-        dt2: datetime = field(
+        a: int
+        b: datetime
+        c: datetime = field(
             serializer=lambda x: x.strftime('%y.%m.%d'), deserializer=lambda x: datetime.strptime(x, '%y.%m.%d')
         )
 
     dt = datetime(2021, 1, 1, 0, 0, 0)
     f = Foo(10, dt, dt)
 
-    assert to_json(f) == '{"i": 10, "dt1": "01/01/21", "dt2": "21.01.01"}'
+    assert to_json(f) == '{"a": 10, "b": "01/01/21", "c": "21.01.01"}'
     assert f == from_json(Foo, to_json(f))
 
     assert to_tuple(f) == (10, '01/01/21', '21.01.01')
@@ -138,14 +155,14 @@ def test_override_by_default_serializer():
 
     @serde(serializer=serializer, deserializer=deserializer)
     class Foo:
-        i: int
-        dt1: datetime
-        dt2: datetime = field(serializer=default_serializer, deserializer=default_deserializer)
+        a: int
+        b: datetime
+        c: datetime = field(serializer=default_serializer, deserializer=default_deserializer)
 
     dt = datetime(2021, 1, 1, 0, 0, 0)
     f = Foo(10, dt, dt)
 
-    assert to_json(f) == '{"i": 10, "dt1": "01/01/21", "dt2": "2021-01-01T00:00:00"}'
+    assert to_json(f) == '{"a": 10, "b": "01/01/21", "c": "2021-01-01T00:00:00"}'
     assert f == from_json(Foo, to_json(f))
 
     assert to_tuple(f) == (10, '01/01/21', datetime(2021, 1, 1, 0, 0))