Repository: yukinarit/pyserde
Pull request #135 (2021-06-04 08:23:44)

feat: Implement custom class (de)serializer
Hi @ydylla  

I've implemented class-level custom (de)serializer. PTAL

I would like to hear your opinion about the attribute names `@deserialize(deserialize=XXX)` and `@serialize(serialize=YYY)`. `serialize` part is repeating in `@serialize` and `serialize=YYY`. They don't look awkward? 🤔  I picked those names in accordance with `serde_serialize`/`serde_deserialize` field attributes. If you know any better name to suggest, I'd appreciate it. Thanks!

```python
def serializer(cls, o):
    if cls is datetime:
        return o.strftime('%d/%m/%y')
    else:
        raise SerdeSkip()

def deserializer(cls, o):
    if cls is datetime:
        return datetime.strptime(o, '%d/%m/%y')
    else:
        raise SerdeSkip()

@deserialize(deserialize=deserializer)
@serialize(serialize=serializer)
@dataclass
class Foo:
    i: int
    dt1: datetime
    dt2: datetime
```


yukinarit: @ydylla  

For the naming, here is the list of candidates

| |  Class   | Field   | Comment  |
|-|---|---|---|
|1| `serialize`/`deserialize`  | `serde_serialize`/`serde_deserialize`  | This is the current implementation.  |
|2| `serializer`/`deserializer`  | `serde_serializer`/`serde_deserializer` | Slightly better than `1` because the name they are different from decorator names? 🤔  |
|3|  `custom`/`custom` | `serde_custom`/`serde_custom`  | Also looks good to me.  |
|4|  `customize`/`customize` | `serde_customize`/`serde_customize`  |  I rather prefer `3` because its' shorter 😄  |
|5|  `converter`/`converter` | `serde_converter`/`serde_converter`  |   |

NOTE:
* I want the same naming for the class/field attributes.
yukinarit: @ydylla  I ponder the names, and think I am going to pick No.2 `serializer/deserializer`, `serde_serializer/serde_deserializer` 👍 

Patch:
diff --git a/README.md b/README.md
index fbce28d..1990655 100644
--- a/README.md
+++ b/README.md
@@ -319,9 +319,9 @@ As you can see below, field is skipped in serialization if `buddy` is "Pikachu".
 
 For complete example, please see [./examples/skip.py](./examples/skip.py)
 
-## Custom serializer/deserializer
+## Custom field serializer/deserializer
 
-If you want to provide a custom function to override the default (de)serialization behaviour of a field, you can pass your function to `serde_serialize` and `serde_deserialize` field attributes.
+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.
 
 ```python
 @deserialize
@@ -331,8 +331,8 @@ class Foo:
     dt1: datetime
     dt2: datetime = field(
         metadata={
-            'serde_serialize': lambda x: x.strftime('%d/%m/%y'),
-            'serde_deserialize': lambda x: datetime.strptime(x, '%d/%m/%y'),
+            'serde_serializer': lambda x: x.strftime('%d/%m/%y'),
+            'serde_deserializer': lambda x: datetime.strptime(x, '%d/%m/%y'),
         }
     )
 ```
@@ -340,6 +340,34 @@ class Foo:
 
 For complete example, please see [./examples/custom_field_serializer.py](./examples/custom_field_serializer.py)
 
+## Custom class serializer/deserializer
+
+If you want to provide (de)serializer at class level, you can pass your functions to `serializer` and `deserializer` class attributes.
+
+```python
+def serializer(cls, o):
+    if cls is datetime:
+        return o.strftime('%d/%m/%y')
+    else:
+        raise SerdeSkip()
+
+def deserializer(cls, o):
+    if cls is datetime:
+        return datetime.strptime(o, '%d/%m/%y')
+    else:
+        raise SerdeSkip()
+
+@deserialize(deserializer=deserializer)
+@serialize(serializer=serializer)
+@dataclass
+class Foo:
+    i: int
+    dt1: datetime
+    dt2: datetime
+```
+
+For complete example, please see [./examples/custom_class_serializer.py](./examples/custom_class_serializer.py)
+
 ## FAQ
 
 ### How can I see the code generated by pyserde?
diff --git a/examples/custom_class_serializer.py b/examples/custom_class_serializer.py
new file mode 100644
index 0000000..01af172
--- /dev/null
+++ b/examples/custom_class_serializer.py
@@ -0,0 +1,58 @@
+from dataclasses import dataclass, field
+from datetime import datetime
+
+from serde import SerdeSkip, default_deserializer, default_serializer, deserialize, serialize
+from serde.json import from_json, to_json
+
+
+def serializer(cls, o):
+    """
+    Custom class level serializer.
+    """
+
+    # You can provide a custom serialize/deserialize logic for certain types.
+    if cls is datetime:
+        return o.strftime('%d/%m/%y')
+    # Raise SerdeSkip to tell serde to use the default serializer/deserializer.
+    else:
+        raise SerdeSkip()
+
+
+def deserializer(cls, o):
+    """
+    Custom class level deserializer.
+    """
+    if cls is datetime:
+        return datetime.strptime(o, '%d/%m/%y')
+    else:
+        raise SerdeSkip()
+
+
+@deserialize(deserializer=deserializer)
+@serialize(serializer=serializer)
+@dataclass
+class Foo:
+    i: int
+    dt1: datetime
+    # Override by field serializer/deserializer.
+    dt2: datetime = field(
+        metadata={
+            'serde_serializer': lambda x: x.strftime('%y.%m.%d'),
+            'serde_deserializer': lambda x: datetime.strptime(x, '%y.%m.%d'),
+        }
+    )
+    # Override by the default serializer/deserializer.
+    dt3: datetime = field(metadata={'serde_serializer': default_serializer, 'serde_deserializer': default_deserializer})
+
+
+def main():
+    dt = datetime(2021, 1, 1, 0, 0, 0)
+    f = Foo(10, dt, dt, dt)
+    print(f"Into Json: {to_json(f)}")
+
+    s = '{"i": 10, "dt1": "01/01/21", "dt2": "01.01.21", "dt3": "2021-01-01T00:00:00"}'
+    print(f"From Json: {from_json(Foo, s)}")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/examples/custom_field_serializer.py b/examples/custom_field_serializer.py
index d3f3586..68ed646 100644
--- a/examples/custom_field_serializer.py
+++ b/examples/custom_field_serializer.py
@@ -12,8 +12,8 @@ class Foo:
     dt1: datetime
     dt2: datetime = field(
         metadata={
-            'serde_serialize': lambda x: x.strftime('%d/%m/%y'),
-            'serde_deserialize': lambda x: datetime.strptime(x, '%d/%m/%y'),
+            'serde_serializer': lambda x: x.strftime('%d/%m/%y'),
+            'serde_deserializer': lambda x: datetime.strptime(x, '%d/%m/%y'),
         }
     )
 
diff --git a/examples/runner.py b/examples/runner.py
index 0286cf9..3411546 100644
--- a/examples/runner.py
+++ b/examples/runner.py
@@ -2,6 +2,7 @@ import sys
 
 import any
 import collection
+import custom_class_serializer
 import custom_field_serializer
 import default
 import env
@@ -30,6 +31,7 @@ def run_all():
     run(tomlfile)
     run(yamlfile)
     run(union)
+    run(custom_class_serializer)
     run(custom_field_serializer)
 
 
diff --git a/serde/__init__.py b/serde/__init__.py
index ae44057..69093a7 100644
--- a/serde/__init__.py
+++ b/serde/__init__.py
@@ -2,9 +2,10 @@
 .. include:: ../README.md
 
 """
-from .core import SerdeError, init, logger  # noqa
-from .de import deserialize, from_dict, from_tuple, is_deserializable  # noqa
-from .se import asdict, astuple, is_serializable, serialize, to_dict, to_tuple  # noqa
+from .compat import SerdeError, SerdeSkip  # noqa
+from .core import init, logger  # noqa
+from .de import default_deserializer, deserialize, from_dict, from_tuple, is_deserializable  # noqa
+from .se import asdict, astuple, default_serializer, is_serializable, serialize, to_dict, to_tuple  # noqa
 
 """ Version of pyserde. """
 __version__ = '0.3.2'
@@ -12,6 +13,8 @@ __version__ = '0.3.2'
 __all__ = [
     'serialize',
     'deserialize',
+    'default_serializer',
+    'default_deserializer',
     'is_serializable',
     'is_deserializable',
     'from_dict',
@@ -19,6 +22,7 @@ __all__ = [
     'to_dict',
     'to_tuple',
     'SerdeError',
+    'SerdeSkip',
     'asdict',
     'astuple',
 ]
diff --git a/serde/compat.py b/serde/compat.py
index 0abd8e3..9cc0d82 100644
--- a/serde/compat.py
+++ b/serde/compat.py
@@ -22,6 +22,12 @@ class SerdeError(TypeError):
     """
 
 
+class SerdeSkip(Exception):
+    """
+    Skip a field in custom (de)serializer.
+    """
+
+
 def get_origin(typ):
     """
     Provide `get_origin` that works in all python versions.
diff --git a/serde/core.py b/serde/core.py
index 1dfa9ee..5ed2da7 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -253,8 +253,8 @@ class Field:
     skip: Optional[bool] = None
     skip_if: Optional[Func] = None
     skip_if_false: Optional[bool] = None
-    serialize: Optional[Func] = None  # Custom field serializer.
-    deserialize: Optional[Func] = None  # Custom field deserializer.
+    serializer: Optional[Func] = None  # Custom field serializer.
+    deserializer: Optional[Func] = None  # Custom field deserializer.
 
     @classmethod
     def from_dataclass(cls, f: dataclasses.Field) -> 'Field':
@@ -268,15 +268,15 @@ class Field:
             if callable(func):
                 skip_if = Func(func, cls.mangle(f, 'skip_if'))
 
-        serialize: Optional[Func] = None
-        func = f.metadata.get('serde_serialize')
+        serializer: Optional[Func] = None
+        func = f.metadata.get('serde_serializer')
         if func:
-            serialize = Func(func, cls.mangle(f, 'serialize'))
+            serializer = Func(func, cls.mangle(f, 'serializer'))
 
-        deserialize: Optional[Func] = None
-        func = f.metadata.get('serde_deserialize')
+        deserializer: Optional[Func] = None
+        func = f.metadata.get('serde_deserializer')
         if func:
-            deserialize = Func(func, cls.mangle(f, 'deserialize'))
+            deserializer = Func(func, cls.mangle(f, 'deserializer'))
 
         return cls(
             f.type,
@@ -286,8 +286,8 @@ class Field:
             rename=f.metadata.get('serde_rename'),
             skip=f.metadata.get('serde_skip'),
             skip_if=skip_if or skip_if_false_func,
-            serialize=serialize,
-            deserialize=deserialize,
+            serializer=serializer,
+            deserializer=deserializer,
         )
 
     @staticmethod
diff --git a/serde/de.py b/serde/de.py
index 85e58bc..9045373 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -25,6 +25,8 @@ from uuid import UUID
 import jinja2
 
 from .compat import (
+    SerdeError,
+    SerdeSkip,
     T,
     has_default,
     has_default_factory,
@@ -52,7 +54,6 @@ from .core import (
     SERDE_SCOPE,
     UNION_DE_PREFIX,
     Field,
-    SerdeError,
     SerdeScope,
     add_func,
     fields,
@@ -64,11 +65,38 @@ from .py36_datetime_compat import py36_date_fromisoformat, py36_datetime_fromiso
 
 __all__: List = ['deserialize', 'is_deserializable', 'Deserializer', 'from_dict', 'from_tuple']
 
-Custom = Optional[Callable[['DeField', Any], Any]]
+# Interface of Custom deserialize function.
+DeserializeFunc = Callable[[Type, Any], Any]
+
+
+def serde_custom_class_deserializer(cls: Type, datavar, value, custom: DeserializeFunc, default: Callable):
+    """
+    Handle custom deserialization. Use default deserialization logic if it receives `SerdeSkip` exception.
+
+    :param cls: Type of the field.
+    :param datavar: The whole variable to deserialize from. e.g. "data"
+    :param value: The value for the field. e.g. "data['i']"
+    :param custom: Custom deserialize function.
+    :param default: Default deserialize function.
+    """
+    try:
+        return custom(cls, value)
+    except SerdeSkip:
+        return default()
+
+
+def default_deserializer(_cls: Type, obj):
+    """
+    Marker function to tell serde to use the default deserializer. It's used when custom deserializer is specified
+    at the class but you want to override a field with the default deserializer.
+    """
 
 
 def deserialize(
-    _cls: Type[T] = None, rename_all: Optional[str] = None, reuse_instances_default: bool = True
+    _cls: Type[T] = None,
+    rename_all: Optional[str] = None,
+    reuse_instances_default: bool = True,
+    deserializer: Optional[DeserializeFunc] = None,
 ) -> Type[T]:
     """
     `deserialize` decorator. A dataclass with this decorator can be deserialized
@@ -119,6 +147,10 @@ def deserialize(
         g['SerdeError'] = SerdeError
         g['raise_unsupported_type'] = raise_unsupported_type
         g['typename'] = typename  # used in union functions
+        if deserialize:
+            g['serde_custom_class_deserializer'] = functools.partial(
+                serde_custom_class_deserializer, custom=deserializer
+            )
 
         # Collect types used in the generated code.
         for typ in iter_types(cls):
@@ -151,11 +183,11 @@ def deserialize(
                 scope.defaults[f.name] = f.default
             elif has_default_factory(f):
                 scope.defaults[f.name] = f.default_factory
-            if f.deserialize:
-                g[f.deserialize.name] = f.deserialize
+            if f.deserializer:
+                g[f.deserializer.name] = f.deserializer
 
-        add_func(scope, FROM_ITER, render_from_iter(cls), g)
-        add_func(scope, FROM_DICT, render_from_dict(cls, rename_all), g)
+        add_func(scope, FROM_ITER, render_from_iter(cls, deserializer), g)
+        add_func(scope, FROM_DICT, render_from_dict(cls, rename_all, deserializer), g)
 
         logger.debug(f'{cls.__name__}: {SERDE_SCOPE} {scope}')
 
@@ -367,12 +399,13 @@ class Renderer:
     """
 
     func: str
+    custom: Optional[DeserializeFunc] = None  # Custom class level deserializer.
 
     def render(self, arg: DeField) -> str:
         """
         Render rvalue
         """
-        if arg.serialize:
+        if arg.deserializer and arg.deserializer.inner is not default_deserializer:
             res = self.custom_field_deserializer(arg)
         elif is_dataclass(arg.type):
             res = self.dataclass(arg)
@@ -437,14 +470,21 @@ class Renderer:
             elif has_default_factory(arg):
                 res = f'({res}) if {exists} else serde_scope.defaults["{arg.name}"]()'
 
-        return 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({arg.type.__name__}, {arg.datavar}, {arg.data}, '
+                f'default=lambda: {res})'
+            )
+        else:
+            return res
 
     def custom_field_deserializer(self, arg: DeField) -> str:
         """
         Render rvalue for the field with custom deserializer.
         """
-        assert arg.deserialize
-        return f"{arg.deserialize.name}({arg.data})"
+        assert arg.deserializer
+        return f"{arg.deserializer.name}({arg.data})"
 
     def dataclass(self, arg: DeField) -> str:
         return f"{arg.type.__name__}.{SERDE_SCOPE}.funcs['{self.func}']({arg.data}, reuse_instances=reuse_instances)"
@@ -605,7 +645,7 @@ def to_iter_arg(f: DeField, *args, **kwargs) -> DeField:
     return f
 
 
-def render_from_iter(cls: Type) -> str:
+def render_from_iter(cls: Type, custom: Optional[DeserializeFunc] = None) -> str:
     template = """
 def {{func}}(data, reuse_instances = {{serde_scope.reuse_instances_default}}):
   if reuse_instances is Ellipsis:
@@ -626,14 +666,14 @@ def {{func}}(data, reuse_instances = {{serde_scope.reuse_instances_default}}):
   )
     """
 
-    renderer = Renderer(FROM_ITER)
+    renderer = Renderer(FROM_ITER, custom)
     env = jinja2.Environment(loader=jinja2.DictLoader({'iter': template}))
     env.filters.update({'rvalue': renderer.render})
     env.filters.update({'arg': to_iter_arg})
     return env.get_template('iter').render(func=FROM_ITER, serde_scope=getattr(cls, SERDE_SCOPE), fields=defields(cls))
 
 
-def render_from_dict(cls: Type, rename_all: Optional[str] = None) -> str:
+def render_from_dict(cls: Type, rename_all: Optional[str] = None, custom: Optional[DeserializeFunc] = None) -> str:
     template = """
 def {{func}}(data, reuse_instances = {{serde_scope.reuse_instances_default}}):
   if reuse_instances is Ellipsis:
@@ -654,7 +694,7 @@ def {{func}}(data, reuse_instances = {{serde_scope.reuse_instances_default}}):
   )
     """
 
-    renderer = Renderer(FROM_DICT)
+    renderer = Renderer(FROM_DICT, custom)
     env = jinja2.Environment(loader=jinja2.DictLoader({'dict': template}))
     env.filters.update({'rvalue': renderer.render})
     env.filters.update({'arg': functools.partial(to_arg, rename_all=rename_all)})
diff --git a/serde/se.py b/serde/se.py
index f7f5aa4..a5da79f 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -17,6 +17,8 @@ from uuid import UUID
 import jinja2
 
 from .compat import (
+    SerdeError,
+    SerdeSkip,
     T,
     is_bare_dict,
     is_bare_list,
@@ -42,7 +44,6 @@ from .core import (
     TO_ITER,
     UNION_SE_PREFIX,
     Field,
-    SerdeError,
     SerdeScope,
     add_func,
     conv,
@@ -55,7 +56,22 @@ from .core import (
 
 __all__: List = ['serialize', 'is_serializable', 'Serializer', 'to_tuple', 'to_dict']
 
-Custom = Optional[Callable[[Any], Any]]
+# Interface of Custom serialize function.
+SerializeFunc = Callable[[Type, Any], Any]
+
+
+def default_serializer(_cls: Type, obj):
+    """
+    Marker function to tell serde to use the default serializer. It's used when custom serializer is specified
+    at the class but you want to override a field with the default serializer.
+    """
+
+
+def serde_custom_class_serializer(cls: Type, obj, custom: SerializeFunc, default: Callable):
+    try:
+        return custom(cls, obj)
+    except SerdeSkip:
+        return default()
 
 
 class Serializer(metaclass=abc.ABCMeta):
@@ -75,6 +91,7 @@ def serialize(
     rename_all: Optional[str] = None,
     reuse_instances_default: bool = True,
     convert_sets_default: bool = False,
+    serializer: Optional[SerializeFunc] = None,
 ) -> Type[T]:
     """
     `serialize` decorator. A dataclass with this decorator can be serialized
@@ -132,6 +149,8 @@ def serialize(
         g['typename'] = typename  # used in union functions
         g['is_instance'] = is_instance  # used in union functions
         g['to_obj'] = to_obj
+        if serialize:
+            g['serde_custom_class_serializer'] = functools.partial(serde_custom_class_serializer, custom=serializer)
 
         # Collect types used in the generated code.
         for typ in iter_types(cls):
@@ -154,11 +173,11 @@ def serialize(
         for f in sefields(cls):
             if f.skip_if:
                 g[f.skip_if.name] = f.skip_if
-            if f.serialize:
-                g[f.serialize.name] = f.serialize
+            if f.serializer:
+                g[f.serializer.name] = f.serializer
 
-        add_func(scope, TO_ITER, render_to_tuple(cls), g)
-        add_func(scope, TO_DICT, render_to_dict(cls, rename_all), g)
+        add_func(scope, TO_ITER, render_to_tuple(cls, serializer), g)
+        add_func(scope, TO_DICT, render_to_dict(cls, rename_all, serializer), g)
 
         logger.debug(f'{cls.__name__}: {SERDE_SCOPE} {scope}')
 
@@ -312,7 +331,7 @@ def sefields(cls: Type) -> Iterator[Field]:
         yield f
 
 
-def render_to_tuple(cls: Type) -> str:
+def render_to_tuple(cls: Type, custom: Optional[SerializeFunc] = None) -> str:
     template = """
 def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
              convert_sets = {{serde_scope.convert_sets_default}}):
@@ -338,13 +357,13 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
   return tuple(res)
     """
 
-    renderer = Renderer(TO_ITER)
+    renderer = Renderer(TO_ITER, custom)
     env = jinja2.Environment(loader=jinja2.DictLoader({'iter': template}))
     env.filters.update({'rvalue': renderer.render})
     return env.get_template('iter').render(func=TO_ITER, serde_scope=getattr(cls, SERDE_SCOPE), fields=sefields(cls))
 
 
-def render_to_dict(cls: Type, case: Optional[str] = None) -> str:
+def render_to_dict(cls: Type, case: Optional[str] = None, custom: Optional[SerializeFunc] = None) -> str:
     template = """
 def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
              convert_sets = {{serde_scope.convert_sets_default}}):
@@ -363,20 +382,19 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
 
   res = {}
   {% for f in fields -%}
-
   {% if not f.skip -%}
     {% if f.skip_if -%}
   if not {{f.skip_if.name}}({{f|rvalue}}):
-   {{f|lvalue}} = {{f|rvalue}}
+    {{f|lvalue}} = {{f|rvalue}}
     {% else -%}
-   {{f|lvalue}} = {{f|rvalue}}
+  {{f|lvalue}} = {{f|rvalue}}
     {% endif -%}
   {% endif %}
 
   {% endfor -%}
   return res
     """
-    renderer = Renderer(TO_DICT)
+    renderer = Renderer(TO_DICT, custom)
     lrenderer = LRenderer(case)
     env = jinja2.Environment(loader=jinja2.DictLoader({'dict': template}))
     env.filters.update({'rvalue': renderer.render})
@@ -436,6 +454,7 @@ class Renderer:
     """
 
     func: str
+    custom: Optional[SerializeFunc] = None  # Custom class level serializer.
 
     def render(self, arg: SeField) -> str:
         """
@@ -471,26 +490,26 @@ convert_sets=convert_sets) for k, v in foo.items()}"
         "(foo[0], foo[1].__serde__.funcs['to_iter'](foo[1], reuse_instances=reuse_instances, \
 convert_sets=convert_sets), foo[2],)"
         """
-        if arg.serialize:
-            return self.custom_field_serializer(arg)
+        if arg.serializer and arg.serializer.inner is not default_serializer:
+            res = self.custom_field_serializer(arg)
         elif is_dataclass(arg.type):
-            return self.dataclass(arg)
+            res = self.dataclass(arg)
         elif is_opt(arg.type):
-            return self.opt(arg)
+            res = self.opt(arg)
         elif is_list(arg.type):
-            return self.list(arg)
+            res = self.list(arg)
         elif is_set(arg.type):
-            return self.set(arg)
+            res = self.set(arg)
         elif is_dict(arg.type):
-            return self.dict(arg)
+            res = self.dict(arg)
         elif is_tuple(arg.type):
-            return self.tuple(arg)
+            res = self.tuple(arg)
         elif is_enum(arg.type):
-            return self.enum(arg)
+            res = self.enum(arg)
         elif is_primitive(arg.type):
-            return self.primitive(arg)
+            res = self.primitive(arg)
         elif is_union(arg.type):
-            return self.union_func(arg)
+            res = self.union_func(arg)
         elif arg.type in [
             Decimal,
             Path,
@@ -507,22 +526,28 @@ convert_sets=convert_sets), foo[2],)"
             IPv4Interface,
             IPv6Interface,
         ]:
-            return f"{arg.varname} if reuse_instances else {self.string(arg)}"
+            res = f"{arg.varname} if reuse_instances else {self.string(arg)}"
         elif arg.type in [date, datetime]:
-            return f"{arg.varname} if reuse_instances else {arg.varname}.isoformat()"
+            res = f"{arg.varname} if reuse_instances else {arg.varname}.isoformat()"
         elif is_none(arg.type):
-            return "None"
+            res = "None"
         elif arg.type is Any:
-            return f"to_obj({arg.varname}, True, False, False)"
+            res = f"to_obj({arg.varname}, True, False, False)"
+        else:
+            res = f"raise_unsupported_type({arg.varname})"
+
+        # Custom field serializer overrides custom class serializer.
+        if self.custom and not arg.serializer:
+            return f'serde_custom_class_serializer({arg.type.__name__}, {arg.varname}, default=lambda: {res})'
         else:
-            return f"raise_unsupported_type({arg.varname})"
+            return res
 
     def custom_field_serializer(self, arg: SeField) -> str:
         """
         Render rvalue for the field with custom serializer.
         """
-        assert arg.serialize
-        return f"{arg.serialize.name}({arg.varname})"
+        assert arg.serializer
+        return f"{arg.serializer.name}({arg.varname})"
 
     def dataclass(self, arg: SeField) -> str:
         """


Test patch:
diff --git a/tests/test_custom.py b/tests/test_custom.py
index b4f3d63..653c757 100644
--- a/tests/test_custom.py
+++ b/tests/test_custom.py
@@ -3,10 +3,11 @@ Tests for custom serializer/deserializer.
 """
 from dataclasses import dataclass, field
 from datetime import datetime
+from typing import Optional
 
 import pytest
 
-from serde import deserialize, serialize
+from serde import SerdeSkip, default_deserializer, default_serializer, deserialize, from_tuple, serialize, to_tuple
 from serde.json import from_json, to_json
 
 
@@ -18,17 +19,26 @@ def test_custom_field_serializer():
         dt1: datetime
         dt2: datetime = field(
             metadata={
-                'serde_serialize': lambda x: x.strftime('%d/%m/%y'),
-                'serde_deserialize': lambda x: datetime.strptime(x, '%d/%m/%y'),
+                'serde_serializer': lambda x: x.strftime('%d/%m/%y'),
+                'serde_deserializer': lambda x: datetime.strptime(x, '%d/%m/%y'),
+            }
+        )
+        dt3: Optional[datetime] = field(
+            metadata={
+                'serde_serializer': lambda x: x.strftime('%d/%m/%y') if x else None,
+                'serde_deserializer': lambda x: datetime.strptime(x, '%d/%m/%y') if x else None,
             }
         )
 
     dt = datetime(2021, 1, 1, 0, 0, 0)
-    f = Foo(dt, dt)
+    f = Foo(dt, dt, None)
 
-    assert to_json(f) == '{"dt1": "2021-01-01T00:00:00", "dt2": "01/01/21"}'
+    assert to_json(f) == '{"dt1": "2021-01-01T00:00:00", "dt2": "01/01/21", "dt3": null}'
     assert f == from_json(Foo, to_json(f))
 
+    assert to_tuple(f) == (datetime(2021, 1, 1, 0, 0), '01/01/21', None)
+    assert f == from_tuple(Foo, to_tuple(f))
+
 
 def test_raise_error():
     def raise_exception(_):
@@ -38,7 +48,7 @@ def test_raise_error():
     @serialize
     @dataclass
     class Foo:
-        i: int = field(metadata={'serde_serialize': raise_exception, 'serde_deserialize': raise_exception})
+        i: int = field(metadata={'serde_serializer': raise_exception, 'serde_deserializer': raise_exception})
 
     f = Foo(10)
     with pytest.raises(Exception):
@@ -53,7 +63,7 @@ def test_wrong_signature():
     @serialize
     @dataclass
     class Foo:
-        i: int = field(metadata={'serde_serialize': lambda: '10', 'serde_deserialize': lambda: 10})
+        i: int = field(metadata={'serde_serializer': lambda: '10', 'serde_deserializer': lambda: 10})
 
     f = Foo(10)
     with pytest.raises(TypeError):
@@ -61,3 +71,103 @@ def test_wrong_signature():
 
     with pytest.raises(TypeError):
         from_json(Foo, '{"i": 10}')
+
+
+def test_custom_class_serializer():
+    def serializer(cls, o):
+        if cls is datetime:
+            return o.strftime('%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    def deserializer(cls, o):
+        if cls is datetime:
+            return datetime.strptime(o, '%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    @deserialize(deserializer=deserializer)
+    @serialize(serializer=serializer)
+    @dataclass
+    class Foo:
+        i: int
+        dt1: datetime
+        dt2: datetime
+
+    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"}'
+    assert f == from_json(Foo, to_json(f))
+
+    assert to_tuple(f) == (10, '01/01/21', '01/01/21')
+    assert f == from_tuple(Foo, to_tuple(f))
+
+
+def test_field_serialize_override_class_serializer():
+    def serializer(cls, o):
+        if cls is datetime:
+            return o.strftime('%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    def deserializer(cls, o):
+        if cls is datetime:
+            return datetime.strptime(o, '%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    @deserialize(deserializer=deserializer)
+    @serialize(serializer=serializer)
+    @dataclass
+    class Foo:
+        i: int
+        dt1: datetime
+        dt2: datetime = field(
+            metadata={
+                'serde_serializer': lambda x: x.strftime('%y.%m.%d'),
+                'serde_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 f == from_json(Foo, to_json(f))
+
+    assert to_tuple(f) == (10, '01/01/21', '21.01.01')
+    assert f == from_tuple(Foo, to_tuple(f))
+
+
+def test_override_by_default_serializer():
+    def serializer(cls, o):
+        if cls is datetime:
+            return o.strftime('%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    def deserializer(cls, o):
+        if cls is datetime:
+            return datetime.strptime(o, '%d/%m/%y')
+        else:
+            raise SerdeSkip()
+
+    @deserialize(deserializer=deserializer)
+    @serialize(serializer=serializer)
+    @dataclass
+    class Foo:
+        i: int
+        dt1: datetime
+        dt2: datetime = field(
+            metadata={'serde_serializer': default_serializer, 'serde_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 f == from_json(Foo, to_json(f))
+
+    assert to_tuple(f) == (10, '01/01/21', datetime(2021, 1, 1, 0, 0))
+    assert f == from_tuple(Foo, to_tuple(f))