Repository: yukinarit/pyserde
Pull request #255 (2022-08-22 13:50:38)

Refactor type check
 * Change to pass "type_check" parameter in decorator 
 * Change default type_check from Coerce to NoCheck


Patch:
diff --git a/docs/features/type-check.md b/docs/features/type-check.md
index 2dd9a04..b753a2b 100644
--- a/docs/features/type-check.md
+++ b/docs/features/type-check.md
@@ -1,10 +1,10 @@
 # Type Checking
 
-`pyserde` provides 3 kinds of type checks.
+This is one of the most awaited features. `pyserde` v0.9 adds the experimental type checkers. As this feature is still experimental, the type checking is not perfect. Also, [@tbsexton](https://github.com/tbsexton) is looking into more beautiful solution, the entire backend of type checker may be replaced by [beartype](https://github.com/beartype/beartype) in the future.
 
 ### `NoCheck`
 
-This is the default behavior until pyserde v0.8.3. No type coercion or checks are applied. If a user puts a wrong value, pyserde doesn't complain anything.
+This is the default behavior until pyserde v0.8.3 and v0.9.x. No type coercion or checks are run. Even if a user puts a wrong value, pyserde doesn't complain anything.
 
 ```python
 @serde
@@ -14,30 +14,37 @@ class Foo
 
 foo = Foo(10)
 # pyserde doesn't complain anything. {"s": 10} will be printed.
-print(to_json(foo, type_check=NoCheck))
-
+print(to_json(foo))
 ```
 
 ### `Coerce`
 
-Type coercion is not yet implemented but this will be the default behavior in pyserde v0.9.0 onward.
+Type coercing automatically converts a value into the declared type during (de)serialization. If the value is incompatible e.g. value is "foo" and type is int, pyserde raises an `SerdeError`.
 
 ```python
+@serde(type_check=Coerce)
+@dataclass
+class Foo
+    s: str
+
 foo = Foo(10)
 # pyserde automatically coerce the int value 10 into "10".
 # {"s": "10"} will be printed.
-print(to_json(foo, type_check=Coerce))
+print(to_json(foo))
 ```
 
-Not yet implemented.
-
 ### `Strict`
 
-Strict type checking is to check every value against the declared type.
+Strict type checking is to check every value against the declared type during (de)serialization. We plan to make `Strict` a default type checker in the future release.
 
 ```python
+@serde(type_check=Strict)
+@dataclass
+class Foo
+    s: str
+
 foo = Foo(10)
 # pyserde checks the value 10 is instance of `str`.
 # SerdeError will be raised in this case because of the type mismatch.
-print(to_json(foo, type_check=Strict))
+print(to_json(foo))
 ```
diff --git a/examples/simple.py b/examples/simple.py
index 7f7acbc..d46e0ad 100644
--- a/examples/simple.py
+++ b/examples/simple.py
@@ -1,10 +1,10 @@
 from dataclasses import dataclass
 
-from serde import serde
+from serde import Strict, serde
 from serde.json import from_json, to_json
 
 
-@serde
+@serde(type_check=Strict)
 @dataclass
 class Foo:
     i: int
diff --git a/serde/__init__.py b/serde/__init__.py
index 9837f0f..cd96548 100644
--- a/serde/__init__.py
+++ b/serde/__init__.py
@@ -54,6 +54,7 @@ from .core import (
     Coerce,
     ExternalTagging,
     InternalTagging,
+    NoCheck,
     Strict,
     Untagged,
     field,
diff --git a/serde/core.py b/serde/core.py
index 48bc163..53eb0cd 100644
--- a/serde/core.py
+++ b/serde/core.py
@@ -677,7 +677,7 @@ def render_type_check(cls: Type) -> str:
     import serde.compat
 
     template = """
-def typecheck(self):
+def {{type_check_func}}(self):
   {% for f in fields -%}
 
   {% if ((is_numpy_available() and is_numpy_type(f.type)) or
@@ -752,13 +752,11 @@ Coerce = TypeCheck(kind=TypeCheck.Kind.Coerce)
 Strict = TypeCheck(kind=TypeCheck.Kind.Strict)
 
 
-def coerce(typ: Type, obj: Any, type_check: TypeCheck) -> Any:
-    return typ(obj) if is_coercible(typ, obj, type_check) else obj
+def coerce(typ: Type, obj: Any) -> Any:
+    return typ(obj) if is_coercible(typ, obj) else obj
 
 
-def is_coercible(typ: Type, obj: Any, type_check: TypeCheck) -> bool:
-    if not type_check.is_coerce():
-        return False
+def is_coercible(typ: Type, obj: Any) -> bool:
     if obj is None:
         return False
     return True
diff --git a/serde/de.py b/serde/de.py
index f0c8ee7..42df90f 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -53,7 +53,6 @@ from .core import (
     SERDE_SCOPE,
     TYPE_CHECK,
     UNION_DE_PREFIX,
-    Coerce,
     DefaultTagging,
     Field,
     NoCheck,
@@ -64,11 +63,7 @@ from .core import (
     coerce,
     ensure,
     fields,
-    is_dict_instance,
     is_instance,
-    is_list_instance,
-    is_set_instance,
-    is_tuple_instance,
     literal_func_name,
     logger,
     raise_unsupported_type,
@@ -143,6 +138,7 @@ def deserialize(
     reuse_instances_default: bool = True,
     deserializer: Optional[DeserializeFunc] = None,
     tagging: Tagging = DefaultTagging,
+    type_check: TypeCheck = NoCheck,
     **kwargs,
 ):
     """
@@ -214,7 +210,7 @@ def deserialize(
         # Create a scope storage used by serde.
         # Each class should get own scope. Child classes can not share scope with parent class.
         # That's why we need the "scope.cls is not cls" check.
-        scope: SerdeScope = getattr(cls, SERDE_SCOPE, None)
+        scope: Optional[SerdeScope] = getattr(cls, SERDE_SCOPE, None)
         if scope is None or scope.cls is not cls:
             scope = SerdeScope(cls, reuse_instances_default=reuse_instances_default)
             setattr(cls, SERDE_SCOPE, scope)
@@ -233,7 +229,7 @@ def deserialize(
         g['get_generic_arg'] = get_generic_arg
         g['is_instance'] = is_instance
         g['TypeCheck'] = TypeCheck
-        g['Coerce'] = Coerce
+        g['NoCheck'] = NoCheck
         g['coerce'] = coerce
         if deserialize:
             g['serde_custom_class_deserializer'] = functools.partial(
@@ -271,8 +267,8 @@ def deserialize(
             if f.deserializer:
                 g[f.deserializer.name] = f.deserializer
 
-        add_func(scope, FROM_ITER, render_from_iter(cls, deserializer), g)
-        add_func(scope, FROM_DICT, render_from_dict(cls, rename_all, deserializer), g)
+        add_func(scope, FROM_ITER, render_from_iter(cls, deserializer, type_check), g)
+        add_func(scope, FROM_DICT, render_from_dict(cls, rename_all, deserializer, type_check), g)
         add_func(scope, TYPE_CHECK, render_type_check(cls), g)
 
         logger.debug(f'{typename(cls)}: {SERDE_SCOPE} {scope}')
@@ -318,16 +314,11 @@ class Deserializer(metaclass=abc.ABCMeta):
         raise NotImplementedError
 
 
-def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: TypeCheck = Coerce):
+def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool):
     """
     Deserialize from an object into an instance of the type specified as arg `c`.
     `c` can be either primitive type, `List`, `Tuple`, `Dict` or `deserialize` class.
-
-    When `type_check`=True, pyserde checks if deserialized field values are equal to the
-    types declared.
     """
-    strict = type_check.is_strict()
-
     if is_generic(c):
         # Store subscripted generic type such as Foo[Bar] in "maybe_generic",
         # and store origin type such as Foo in "c". Since subscripted generics
@@ -338,17 +329,13 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
     else:
         maybe_generic = c
     try:
-        thisfunc = functools.partial(from_obj, named=named, reuse_instances=reuse_instances, type_check=type_check)
+        thisfunc = functools.partial(from_obj, named=named, reuse_instances=reuse_instances)
         if o is None:
             return None
         if is_deserializable(c):
             serde_scope: SerdeScope = getattr(c, SERDE_SCOPE)
             func_name = FROM_DICT if named else FROM_ITER
-            res = serde_scope.funcs[func_name](
-                c, maybe_generic=maybe_generic, data=o, reuse_instances=reuse_instances, type_check=type_check
-            )
-            if strict:
-                serde_scope.funcs[TYPE_CHECK](res)
+            res = serde_scope.funcs[func_name](c, maybe_generic=maybe_generic, data=o, reuse_instances=reuse_instances)
             return res
         elif is_opt(c):
             if o is None:
@@ -368,40 +355,25 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
             if is_bare_list(c):
                 return [e for e in o]
             else:
-                res = [thisfunc(type_args(c)[0], e, type_check=type_check) for e in o]
-                if strict:
-                    if not is_list_instance(res, c):
-                        raise SerdeError(f"Failed to deserialize into {typename(c)}")
+                res = [thisfunc(type_args(c)[0], e) for e in o]
                 return res
         elif is_set(c):
             if is_bare_set(c):
                 return set(e for e in o)
             else:
-                res = set(thisfunc(type_args(c)[0], e, type_check=NoCheck) for e in o)
-                if strict:
-                    if not is_set_instance(res, c):
-                        raise SerdeError(f"Failed to deserialize into {typename(c)}")
+                res = set(thisfunc(type_args(c)[0], e) for e in o)
                 return res
         elif is_tuple(c):
             if is_bare_tuple(c):
                 return tuple(e for e in o)
             else:
                 res = tuple(thisfunc(type_args(c)[i], e) for i, e in enumerate(o))
-                if strict:
-                    if not is_tuple_instance(res, c):
-                        raise SerdeError(f"Failed to deserialize into {typename(c)}")
                 return res
         elif is_dict(c):
             if is_bare_dict(c):
                 return {k: v for k, v in o.items()}
             else:
-                res = {
-                    thisfunc(type_args(c)[0], k, type_check=NoCheck): thisfunc(type_args(c)[1], v, type_check=NoCheck)
-                    for k, v in o.items()
-                }
-                if strict:
-                    if not is_dict_instance(res, c):
-                        raise SerdeError(f"Failed to deserialize into {typename(c)}")
+                res = {thisfunc(type_args(c)[0], k): thisfunc(type_args(c)[1], v) for k, v in o.items()}
                 return res
         elif is_numpy_array(c):
             return deserialize_numpy_array_direct(c, o)
@@ -419,7 +391,7 @@ def from_obj(c: Type, o: Any, named: bool, reuse_instances: bool, type_check: Ty
         raise SerdeError(e)
 
 
-def from_dict(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerce):
+def from_dict(cls, o, reuse_instances: bool = ...):
     """
     Deserialize dictionary into object.
 
@@ -439,10 +411,10 @@ def from_dict(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerc
     >>> from_dict(List[Foo], lst)
     [Foo(i=10, s='foo', f=100.0, b=True), Foo(i=20, s='foo', f=100.0, b=True)]
     """
-    return from_obj(cls, o, named=True, reuse_instances=reuse_instances, type_check=type_check)
+    return from_obj(cls, o, named=True, reuse_instances=reuse_instances)
 
 
-def from_tuple(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coerce):
+def from_tuple(cls, o, reuse_instances: bool = ...):
     """
     Deserialize tuple into object.
 
@@ -462,7 +434,7 @@ def from_tuple(cls, o, reuse_instances: bool = ..., type_check: TypeCheck = Coer
     >>> from_tuple(List[Foo], lst)
     [Foo(i=10, s='foo', f=100.0, b=True), Foo(i=20, s='foo', f=100.0, b=True)]
     """
-    return from_obj(cls, o, named=False, reuse_instances=reuse_instances, type_check=type_check)
+    return from_obj(cls, o, named=False, reuse_instances=reuse_instances)
 
 
 @dataclass
@@ -616,7 +588,7 @@ class Renderer:
 
         if self.custom and not arg.deserializer:
             # Rerender the code for default deserializer.
-            default = Renderer(self.func, self.cls, None).render(arg)
+            default = Renderer(self.func, self.cls, None, suppress_coerce=self.suppress_coerce).render(arg)
             return self.custom_class_deserializer(arg, default)
         else:
             return res
@@ -660,13 +632,13 @@ class Renderer:
 
         >>> from typing import List
         >>> Renderer('foo').render(DeField(Optional[int], 'o', datavar='data'))
-        '(coerce(int, data["o"], type_check)) if data.get("o") is not None else None'
+        '(coerce(int, data["o"])) if data.get("o") is not None else None'
 
         >>> Renderer('foo').render(DeField(Optional[List[int]], 'o', datavar='data'))
-        '([coerce(int, v, type_check) for v in data["o"]]) if data.get("o") is not None else None'
+        '([coerce(int, v) for v in data["o"]]) if data.get("o") is not None else None'
 
         >>> Renderer('foo').render(DeField(Optional[List[int]], 'o', datavar='data'))
-        '([coerce(int, v, type_check) for v in data["o"]]) if data.get("o") is not None else None'
+        '([coerce(int, v) for v in data["o"]]) if data.get("o") is not None else None'
 
         >>> @deserialize
         ... class Foo:
@@ -691,10 +663,10 @@ if data.get("f") is not None else None'
 
         >>> from typing import List
         >>> Renderer('foo').render(DeField(List[int], 'l', datavar='data'))
-        '[coerce(int, v, type_check) for v in data["l"]]'
+        '[coerce(int, v) for v in data["l"]]'
 
         >>> Renderer('foo').render(DeField(List[List[int]], 'l', datavar='data'))
-        '[[coerce(int, v, type_check) for v in v] for v in data["l"]]'
+        '[[coerce(int, v) for v in v] for v in data["l"]]'
         """
         if is_bare_list(arg.type):
             return f'list({arg.data})'
@@ -707,10 +679,10 @@ if data.get("f") is not None else None'
 
         >>> from typing import Set
         >>> Renderer('foo').render(DeField(Set[int], 'l', datavar='data'))
-        'set(coerce(int, v, type_check) for v in data["l"])'
+        'set(coerce(int, v) for v in data["l"])'
 
         >>> Renderer('foo').render(DeField(Set[Set[int]], 'l', datavar='data'))
-        'set(set(coerce(int, v, type_check) for v in v) for v in data["l"])'
+        'set(set(coerce(int, v) for v in v) for v in data["l"])'
         """
         if is_bare_set(arg.type):
             return f'set({arg.data})'
@@ -725,14 +697,14 @@ if data.get("f") is not None else None'
         >>> @deserialize
         ... class Foo: pass
         >>> Renderer('foo').render(DeField(Tuple[str, int, List[int], Foo], 'd', datavar='data'))
-        '(coerce(str, data["d"][0], type_check), coerce(int, data["d"][1], type_check), \
-[coerce(int, v, type_check) for v in data["d"][2]], \
+        '(coerce(str, data["d"][0]), coerce(int, data["d"][1]), \
+[coerce(int, v) for v in data["d"][2]], \
 Foo.__serde__.funcs[\\'foo\\'](data=data["d"][3], reuse_instances=reuse_instances),)'
 
         >>> field = DeField(Tuple[str, int, List[int], Foo], 'd', datavar='data', index=0, iterbased=True)
         >>> Renderer('foo').render(field)
-        "(coerce(str, data[0][0], type_check), coerce(int, data[0][1], type_check), \
-[coerce(int, v, type_check) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
+        "(coerce(str, data[0][0]), coerce(int, data[0][1]), \
+[coerce(int, v) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
 reuse_instances=reuse_instances),)"
         """
         if is_bare_tuple(arg.type):
@@ -750,7 +722,7 @@ reuse_instances=reuse_instances),)"
 
         >>> from typing import List
         >>> Renderer('foo').render(DeField(Dict[str, int], 'd', datavar='data'))
-        '{coerce(str, k, type_check): coerce(int, v, type_check) for k, v in data["d"].items()}'
+        '{coerce(str, k): coerce(int, v) for k, v in data["d"].items()}'
 
         >>> @deserialize
         ... class Foo: pass
@@ -773,20 +745,20 @@ reuse_instances=reuse_instances),)"
         Render rvalue for primitives.
 
         >>> Renderer('foo').render(DeField(int, 'i', datavar='data'))
-        'coerce(int, data["i"], type_check)'
+        'coerce(int, data["i"])'
 
         >>> Renderer('foo').render(DeField(int, 'int_field', datavar='data', case='camelcase'))
-        'coerce(int, data["intField"], type_check)'
+        'coerce(int, data["intField"])'
 
         >>> Renderer('foo').render(DeField(int, 'i', datavar='data', index=1, iterbased=True))
-        'coerce(int, data[1], type_check)'
+        'coerce(int, data[1])'
         """
         typ = typename(arg.type)
         dat = arg.data
         if self.suppress_coerce:
             return dat
         else:
-            return f'coerce({typ}, {dat}, type_check)'
+            return f'coerce({typ}, {dat})'
 
     def c_tor(self, arg: DeField) -> str:
         return f"{typename(arg.type)}({arg.data})"
@@ -827,10 +799,9 @@ def to_iter_arg(f: DeField, *args, **kwargs) -> DeField:
     return f
 
 
-def render_from_iter(cls: Type, custom: Optional[DeserializeFunc] = None) -> str:
+def render_from_iter(cls: Type, custom: Optional[DeserializeFunc] = None, type_check: TypeCheck = NoCheck) -> str:
     template = """
-def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}},
-             type_check: TypeCheck=Coerce):
+def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}}):
   if reuse_instances is Ellipsis:
     reuse_instances = {{serde_scope.reuse_instances_default}}
 
@@ -851,7 +822,7 @@ def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_s
     raise UserError(e)
     """
 
-    renderer = Renderer(FROM_ITER, cls=cls, custom=custom)
+    renderer = Renderer(FROM_ITER, cls=cls, custom=custom, suppress_coerce=(not type_check.is_coerce()))
     env = jinja2.Environment(loader=jinja2.DictLoader({'iter': template}))
     env.filters.update({'rvalue': renderer.render})
     env.filters.update({'arg': to_iter_arg})
@@ -863,11 +834,15 @@ def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_s
     return res
 
 
-def render_from_dict(cls: Type, rename_all: Optional[str] = None, custom: Optional[DeserializeFunc] = None) -> str:
+def render_from_dict(
+    cls: Type,
+    rename_all: Optional[str] = None,
+    custom: Optional[DeserializeFunc] = None,
+    type_check: TypeCheck = NoCheck,
+) -> str:
     template = """
 def {{func}}(cls=cls, maybe_generic=None, data=None,
-             reuse_instances = {{serde_scope.reuse_instances_default}},
-             type_check: TypeCheck = Coerce):
+             reuse_instances = {{serde_scope.reuse_instances_default}}):
   if reuse_instances is Ellipsis:
     reuse_instances = {{serde_scope.reuse_instances_default}}
 
@@ -887,14 +862,20 @@ def {{func}}(cls=cls, maybe_generic=None, data=None,
   except Exception as e:
     raise UserError(e)
 
+  {% if type_check.is_strict() %}
+  rv.__serde__.funcs['typecheck'](rv)
+  {% endif %}
+
   return rv
     """
 
-    renderer = Renderer(FROM_DICT, cls=cls, custom=custom)
+    renderer = Renderer(FROM_DICT, cls=cls, custom=custom, suppress_coerce=(not type_check.is_coerce()))
     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)})
-    res = env.get_template('dict').render(func=FROM_DICT, serde_scope=getattr(cls, SERDE_SCOPE), fields=defields(cls))
+    res = env.get_template('dict').render(
+        func=FROM_DICT, serde_scope=getattr(cls, SERDE_SCOPE), fields=defields(cls), type_check=type_check
+    )
 
     if renderer.import_numpy:
         res = "import numpy\n" + res
@@ -904,8 +885,7 @@ def {{func}}(cls=cls, maybe_generic=None, data=None,
 
 def render_union_func(cls: Type, union_args: List[Type], tagging: Tagging = DefaultTagging) -> str:
     template = """
-def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}},
-             type_check: TypeCheck=Coerce):
+def {{func}}(cls=cls, maybe_generic=None, data=None, reuse_instances = {{serde_scope.reuse_instances_default}}):
   errors = []
   {% for t in union_args %}
   try:
diff --git a/serde/json.py b/serde/json.py
index 50b1aec..348734f 100644
--- a/serde/json.py
+++ b/serde/json.py
@@ -4,7 +4,6 @@ Serialize and Deserialize in JSON format.
 from typing import Any, Type, Union
 
 from .compat import T
-from .core import Coerce, TypeCheck
 from .de import Deserializer, from_dict
 from .numpy import encode_numpy
 from .se import Serializer, to_dict
@@ -50,7 +49,7 @@ class JsonDeserializer(Deserializer):
         return json_loads(s, **opts)
 
 
-def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
+def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, **opts) -> str:
     """
     Serialize the object into JSON str. [orjson](https://github.com/ijl/orjson) will be used if installed.
 
@@ -61,12 +60,10 @@ def to_json(obj: Any, se: Type[Serializer] = JsonSerializer, type_check: TypeChe
 
     If you want to use another json package, you can subclass `JsonSerializer` and implement your own logic.
     """
-    return se.serialize(to_dict(obj, reuse_instances=False, convert_sets=True, type_check=type_check), **opts)
+    return se.serialize(to_dict(obj, reuse_instances=False, convert_sets=True), **opts)
 
 
-def from_json(
-    c: Type[T], s: Union[str, bytes], de: Type[Deserializer] = JsonDeserializer, type_check: TypeCheck = Coerce, **opts
-) -> T:
+def from_json(c: Type[T], s: Union[str, bytes], de: Type[Deserializer] = JsonDeserializer, **opts) -> T:
     """
     Deserialize from JSON into the object. [orjson](https://github.com/ijl/orjson) will be used if installed.
 
@@ -75,4 +72,4 @@ def from_json(
 
     If you want to use another json package, you can subclass `JsonDeserializer` and implement your own logic.
     """
-    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False, type_check=type_check)
+    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
diff --git a/serde/msgpack.py b/serde/msgpack.py
index 32090ad..a40027a 100644
--- a/serde/msgpack.py
+++ b/serde/msgpack.py
@@ -7,7 +7,7 @@ from typing import Any, Dict, Type
 import msgpack
 
 from .compat import T
-from .core import Coerce, SerdeError, TypeCheck
+from .core import SerdeError
 from .de import Deserializer, from_dict, from_tuple
 from .numpy import encode_numpy
 from .se import Serializer, to_dict, to_tuple
@@ -39,7 +39,6 @@ def to_msgpack(
     se: Type[Serializer] = MsgPackSerializer,
     named: bool = True,
     ext_dict: Dict[Type, int] = None,
-    type_check: TypeCheck = Coerce,
     **opts,
 ) -> bytes:
     """
@@ -63,7 +62,7 @@ def to_msgpack(
 
     to_func = to_dict if named else to_tuple
     return se.serialize(
-        to_func(obj, reuse_instances=False, convert_sets=True, type_check=type_check),
+        to_func(obj, reuse_instances=False, convert_sets=True),
         ext_type_code=ext_type_code,
         **opts,
     )
@@ -75,7 +74,6 @@ def from_msgpack(
     de: Type[Deserializer] = MsgPackDeserializer,
     named: bool = True,
     ext_dict: Dict[int, Type] = None,
-    type_check: TypeCheck = Coerce,
     **opts,
 ) -> Type[T]:
     """
@@ -92,7 +90,7 @@ def from_msgpack(
         ext_type = ext_dict.get(ext.code)
         if ext_type is None:
             raise SerdeError(f"Could not find type for code {ext.code} in ext_dict")
-        return from_msgpack(ext_type, ext.data, de, named, type_check=type_check, **opts)
+        return from_msgpack(ext_type, ext.data, de, named, **opts)
     else:
         from_func = from_dict if named else from_tuple
-        return from_func(c, de.deserialize(s, **opts), reuse_instances=False, type_check=type_check)
+        return from_func(c, de.deserialize(s, **opts), reuse_instances=False)
diff --git a/serde/se.py b/serde/se.py
index 94dde18..ad6f163 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -50,9 +50,9 @@ from .core import (
     TO_ITER,
     TYPE_CHECK,
     UNION_SE_PREFIX,
-    Coerce,
     DefaultTagging,
     Field,
+    NoCheck,
     SerdeScope,
     Tagging,
     TypeCheck,
@@ -60,11 +60,7 @@ from .core import (
     coerce,
     conv,
     fields,
-    is_dict_instance,
     is_instance,
-    is_list_instance,
-    is_set_instance,
-    is_tuple_instance,
     logger,
     raise_unsupported_type,
     render_type_check,
@@ -143,6 +139,7 @@ def serialize(
     convert_sets_default: bool = False,
     serializer: Optional[SerializeFunc] = None,
     tagging: Tagging = DefaultTagging,
+    type_check: TypeCheck = NoCheck,
     **kwargs,
 ):
     """
@@ -214,7 +211,7 @@ def serialize(
         # Create a scope storage used by serde.
         # Each class should get own scope. Child classes can not share scope with parent class.
         # That's why we need the "scope.cls is not cls" check.
-        scope: SerdeScope = getattr(cls, SERDE_SCOPE, None)
+        scope: Optional[SerdeScope] = getattr(cls, SERDE_SCOPE, None)
         if scope is None or scope.cls is not cls:
             scope = SerdeScope(
                 cls,
@@ -237,7 +234,7 @@ def serialize(
         g["typing"] = typing
         g["Literal"] = Literal
         g["TypeCheck"] = TypeCheck
-        g["Coerce"] = Coerce
+        g["NoCheck"] = NoCheck
         g["coerce"] = coerce
         if serialize:
             g["serde_custom_class_serializer"] = functools.partial(serde_custom_class_serializer, custom=serializer)
@@ -261,8 +258,8 @@ def serialize(
             if f.serializer:
                 g[f.serializer.name] = f.serializer
 
-        add_func(scope, TO_ITER, render_to_tuple(cls, serializer), g)
-        add_func(scope, TO_DICT, render_to_dict(cls, rename_all, serializer), g)
+        add_func(scope, TO_ITER, render_to_tuple(cls, serializer, type_check), g)
+        add_func(scope, TO_DICT, render_to_dict(cls, rename_all, serializer, type_check), g)
         add_func(scope, TYPE_CHECK, render_type_check(cls), g)
 
         logger.debug(f"{typename(cls)}: {SERDE_SCOPE} {scope}")
@@ -294,9 +291,7 @@ def is_serializable(instance_or_class: Any) -> bool:
     return hasattr(instance_or_class, SERDE_SCOPE)
 
 
-def to_obj(o, named: bool, reuse_instances: bool, convert_sets: bool, c: Type = None, type_check: TypeCheck = Coerce):
-    strict = type_check.is_strict()
-
+def to_obj(o, named: bool, reuse_instances: bool, convert_sets: bool, c: Type = None):
     try:
         thisfunc = functools.partial(
             to_obj,
@@ -309,37 +304,19 @@ def to_obj(o, named: bool, reuse_instances: bool, convert_sets: bool, c: Type =
         if is_serializable(o):
             serde_scope: SerdeScope = getattr(o, SERDE_SCOPE)
             func_name = TO_DICT if named else TO_ITER
-            if strict:
-                serde_scope.funcs[TYPE_CHECK](o)
-            return serde_scope.funcs[func_name](
-                o, reuse_instances=reuse_instances, convert_sets=convert_sets, type_check=type_check
-            )
+            return serde_scope.funcs[func_name](o, reuse_instances=reuse_instances, convert_sets=convert_sets)
         elif is_dataclass(o):
-            if strict:
-                serde_scope.funcs[TYPE_CHECK](o)
             if named:
                 return dataclasses.asdict(o)
             else:
                 return dataclasses.astuple(o)
         elif isinstance(o, list):
-            if strict and c:
-                if is_list_instance(o, c):
-                    raise SerdeError(f"Failed to deserialize into {typename(c)}")
             return [thisfunc(e) for e in o]
         elif isinstance(o, tuple):
-            if strict and c:
-                if is_tuple_instance(o, c):
-                    raise SerdeError(f"Failed to deserialize into {typename(c)}")
             return tuple(thisfunc(e) for e in o)
         elif isinstance(o, set):
-            if strict and c:
-                if is_set_instance(o, c):
-                    raise SerdeError(f"Failed to deserialize into {typename(c)}")
             return [thisfunc(e) for e in o]
         elif isinstance(o, dict):
-            if strict and c:
-                if is_dict_instance(o, c):
-                    raise SerdeError(f"Failed to deserialize into {typename(c)}")
             return {k: thisfunc(v) for k, v in o.items()}
         elif is_str_serializable_instance(o):
             return str(o)
@@ -359,7 +336,7 @@ def astuple(v):
     return to_tuple(v, reuse_instances=False, convert_sets=False)
 
 
-def to_tuple(o, reuse_instances: bool = ..., convert_sets: bool = ..., type_check: TypeCheck = Coerce) -> Any:
+def to_tuple(o, reuse_instances: bool = ..., convert_sets: bool = ...) -> Any:
     """
     Serialize object into tuple.
 
@@ -379,7 +356,7 @@ def to_tuple(o, reuse_instances: bool = ..., convert_sets: bool = ..., type_chec
     >>> to_tuple(lst)
     [(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]
     """
-    return to_obj(o, named=False, reuse_instances=reuse_instances, convert_sets=convert_sets, type_check=type_check)
+    return to_obj(o, named=False, reuse_instances=reuse_instances, convert_sets=convert_sets)
 
 
 def asdict(v):
@@ -389,7 +366,7 @@ def asdict(v):
     return to_dict(v, reuse_instances=False, convert_sets=False)
 
 
-def to_dict(o, reuse_instances: bool = ..., convert_sets: bool = ..., type_check: TypeCheck = Coerce) -> Any:
+def to_dict(o, reuse_instances: bool = ..., convert_sets: bool = ...) -> Any:
     """
     Serialize object into dictionary.
 
@@ -409,7 +386,7 @@ def to_dict(o, reuse_instances: bool = ..., convert_sets: bool = ..., type_check
     >>> to_dict(lst)
     [{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
     """
-    return to_obj(o, named=True, reuse_instances=reuse_instances, convert_sets=convert_sets, type_check=type_check)
+    return to_obj(o, named=True, reuse_instances=reuse_instances, convert_sets=convert_sets)
 
 
 @dataclass
@@ -448,11 +425,10 @@ def sefields(cls: Type) -> Iterator[SeField]:
         yield f
 
 
-def render_to_tuple(cls: Type, custom: Optional[SerializeFunc] = None) -> str:
+def render_to_tuple(cls: Type, custom: Optional[SerializeFunc] = None, type_check: TypeCheck = NoCheck) -> str:
     template = """
 def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
-             convert_sets = {{serde_scope.convert_sets_default}},
-             type_check: TypeCheck=Coerce):
+             convert_sets = {{serde_scope.convert_sets_default}}):
   if reuse_instances is Ellipsis:
     reuse_instances = {{serde_scope.reuse_instances_default}}
   if convert_sets is Ellipsis:
@@ -461,6 +437,10 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
   if not is_dataclass(obj):
     return copy.deepcopy(obj)
 
+  {% if type_check.is_strict() %}
+  obj.__serde__.funcs['typecheck'](obj)
+  {% endif %}
+
   return (
   {% for f in fields -%}
   {% if not f.skip|default(False) %}
@@ -470,17 +450,20 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
   )
     """
 
-    renderer = Renderer(TO_ITER, custom)
+    renderer = Renderer(TO_ITER, custom, suppress_coerce=(not type_check.is_coerce()))
     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))
+    return env.get_template("iter").render(
+        func=TO_ITER, serde_scope=getattr(cls, SERDE_SCOPE), fields=sefields(cls), type_check=type_check
+    )
 
 
-def render_to_dict(cls: Type, case: Optional[str] = None, custom: Optional[SerializeFunc] = None) -> str:
+def render_to_dict(
+    cls: Type, case: Optional[str] = None, custom: Optional[SerializeFunc] = None, type_check: TypeCheck = NoCheck
+) -> str:
     template = """
 def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
-             convert_sets = {{serde_scope.convert_sets_default}},
-             type_check: TypeCheck = Coerce):
+             convert_sets = {{serde_scope.convert_sets_default}}):
   if reuse_instances is Ellipsis:
     reuse_instances = {{serde_scope.reuse_instances_default}}
   if convert_sets is Ellipsis:
@@ -489,6 +472,10 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
   if not is_dataclass(obj):
     return copy.deepcopy(obj)
 
+  {% if type_check.is_strict() %}
+  obj.__serde__.funcs['typecheck'](obj)
+  {% endif %}
+
   res = {}
   {% for f in fields -%}
   {% if not f.skip -%}
@@ -503,13 +490,15 @@ def {{func}}(obj, reuse_instances = {{serde_scope.reuse_instances_default}},
   {% endfor -%}
   return res
     """
-    renderer = Renderer(TO_DICT, custom)
+    renderer = Renderer(TO_DICT, custom, suppress_coerce=(not type_check.is_coerce()))
     lrenderer = LRenderer(case)
     env = jinja2.Environment(loader=jinja2.DictLoader({"dict": template}))
     env.filters.update({"rvalue": renderer.render})
     env.filters.update({"lvalue": lrenderer.render})
     env.filters.update({"case": functools.partial(conv, case=case)})
-    return env.get_template("dict").render(func=TO_DICT, serde_scope=getattr(cls, SERDE_SCOPE), fields=sefields(cls))
+    return env.get_template("dict").render(
+        func=TO_DICT, serde_scope=getattr(cls, SERDE_SCOPE), fields=sefields(cls), type_check=type_check
+    )
 
 
 def render_union_func(cls: Type, union_args: List[Type], tagging: Tagging = DefaultTagging) -> str:
@@ -517,7 +506,7 @@ def render_union_func(cls: Type, union_args: List[Type], tagging: Tagging = Defa
     Render function that serializes a field with union type.
     """
     template = """
-def {{func}}(obj, reuse_instances, convert_sets, type_check: TypeCheck=Coerce):
+def {{func}}(obj, reuse_instances, convert_sets):
   union_args = serde_scope.union_se_args['{{func}}']
 
   {% for t in union_args %}
@@ -594,6 +583,7 @@ class Renderer:
     func: str
     custom: Optional[SerializeFunc] = None  # Custom class level serializer.
     suppress_coerce: bool = False
+    """ Suppress type coercing because generated union serializer has its own type checking """
 
     def render(self, arg: SeField) -> str:
         """
@@ -601,10 +591,10 @@ class Renderer:
 
         >>> from typing import Tuple
         >>> Renderer(TO_ITER).render(SeField(int, 'i'))
-        'coerce(int, i, type_check)'
+        'coerce(int, i)'
 
         >>> Renderer(TO_ITER).render(SeField(List[int], 'l'))
-        '[coerce(int, v, type_check) for v in l]'
+        '[coerce(int, v) for v in l]'
 
         >>> @serialize
         ... @dataclass(unsafe_hash=True)
@@ -617,7 +607,7 @@ class Renderer:
         "[v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, convert_sets=convert_sets) for v in foo]"
 
         >>> Renderer(TO_ITER).render(SeField(Dict[str, Foo], 'foo'))
-        "{coerce(str, k, type_check): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \
+        "{coerce(str, k): v.__serde__.funcs['to_iter'](v, reuse_instances=reuse_instances, \
 convert_sets=convert_sets) for k, v in foo.items()}"
 
         >>> Renderer(TO_ITER).render(SeField(Dict[Foo, Foo], 'foo'))
@@ -626,8 +616,8 @@ convert_sets=convert_sets): v.__serde__.funcs['to_iter'](v, reuse_instances=reus
 convert_sets=convert_sets) for k, v in foo.items()}"
 
         >>> Renderer(TO_ITER).render(SeField(Tuple[str, Foo, int], 'foo'))
-        "(coerce(str, foo[0], type_check), foo[1].__serde__.funcs['to_iter'](foo[1], reuse_instances=reuse_instances, \
-convert_sets=convert_sets), coerce(int, foo[2], type_check),)"
+        "(coerce(str, foo[0]), foo[1].__serde__.funcs['to_iter'](foo[1], reuse_instances=reuse_instances, \
+convert_sets=convert_sets), coerce(int, foo[2]),)"
         """
         if arg.serializer and arg.serializer.inner is not default_serializer:
             res = self.custom_field_serializer(arg)
@@ -775,7 +765,7 @@ convert_sets=convert_sets), coerce(int, foo[2], type_check),)"
         if self.suppress_coerce:
             return var
         else:
-            return f'coerce({typ}, {var}, type_check)'
+            return f'coerce({typ}, {var})'
 
     def string(self, arg: SeField) -> str:
         return f"str({arg.varname})"
diff --git a/serde/toml.py b/serde/toml.py
index efbd0f4..2bcfe3b 100644
--- a/serde/toml.py
+++ b/serde/toml.py
@@ -7,7 +7,6 @@ import tomli
 import tomli_w
 
 from .compat import T
-from .core import Coerce, TypeCheck
 from .de import Deserializer, from_dict
 from .se import Serializer, to_dict
 
@@ -26,7 +25,7 @@ class TomlDeserializer(Deserializer):
         return tomli.loads(s, **opts)
 
 
-def to_toml(obj, se: Type[Serializer] = TomlSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
+def to_toml(obj, se: Type[Serializer] = TomlSerializer, **opts) -> str:
     """
     Serialize the object into TOML.
 
@@ -35,12 +34,10 @@ def to_toml(obj, se: Type[Serializer] = TomlSerializer, type_check: TypeCheck =
 
     If you want to use the other toml package, you can subclass `TomlSerializer` and implement your own logic.
     """
-    return se.serialize(to_dict(obj, reuse_instances=False, type_check=type_check), **opts)
+    return se.serialize(to_dict(obj, reuse_instances=False), **opts)
 
 
-def from_toml(
-    c: Type[T], s: str, de: Type[Deserializer] = TomlDeserializer, type_check: TypeCheck = Coerce, **opts
-) -> T:
+def from_toml(c: Type[T], s: str, de: Type[Deserializer] = TomlDeserializer, **opts) -> T:
     """
     Deserialize from TOML into the object.
 
@@ -49,4 +46,4 @@ def from_toml(
 
     If you want to use the other toml package, you can subclass `TomlDeserializer` and implement your own logic.
     """
-    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False, type_check=type_check)
+    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
diff --git a/serde/yaml.py b/serde/yaml.py
index a989eea..8d9a845 100644
--- a/serde/yaml.py
+++ b/serde/yaml.py
@@ -6,7 +6,6 @@ from typing import Type
 import yaml
 
 from .compat import T
-from .core import Coerce, TypeCheck
 from .de import Deserializer, from_dict
 from .se import Serializer, to_dict
 
@@ -25,7 +24,7 @@ class YamlDeserializer(Deserializer):
         return yaml.safe_load(s, **opts)
 
 
-def to_yaml(obj, se: Type[Serializer] = YamlSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
+def to_yaml(obj, se: Type[Serializer] = YamlSerializer, **opts) -> str:
     """
     Serialize the object into YAML.
 
@@ -34,16 +33,14 @@ def to_yaml(obj, se: Type[Serializer] = YamlSerializer, type_check: TypeCheck =
 
     If you want to use the other yaml package, you can subclass `YamlSerializer` and implement your own logic.
     """
-    return se.serialize(to_dict(obj, reuse_instances=False, type_check=type_check), **opts)
+    return se.serialize(to_dict(obj, reuse_instances=False), **opts)
 
 
-def from_yaml(
-    c: Type[T], s: str, de: Type[Deserializer] = YamlDeserializer, type_check: TypeCheck = Coerce, **opts
-) -> T:
+def from_yaml(c: Type[T], s: str, de: Type[Deserializer] = YamlDeserializer, **opts) -> T:
     """
     `c` is a class obejct and `s` is YAML string. If you supply keyword arguments other than `de`,
     they will be passed in `yaml.safe_load` function.
 
     If you want to use the other yaml package, you can subclass `YamlDeserializer` and implement your own logic.
     """
-    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False, type_check=type_check)
+    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 488eaf3..6e3cd96 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -47,7 +47,6 @@ def test_simple(se, de, opt, t, T, f):
 
     c = C(10, t)
     assert c == de(C, se(c))
-    assert c == de(C, se(c, type_check=Strict), type_check=Strict)
 
     @serde.serde(**opt)
     class Nested:
@@ -59,11 +58,9 @@ def test_simple(se, de, opt, t, T, f):
 
     c = C(Nested(t))
     assert c == de(C, se(c))
-    assert c == de(C, se(c, type_check=Strict), type_check=Strict)
 
     if se is not serde.toml.to_toml:
         assert t == de(T, se(t))
-        assert t == de(T, se(t, type_check=Strict), type_check=Strict)
 
 
 @pytest.mark.parametrize('t,T,filter', types, ids=type_ids())
@@ -773,21 +770,21 @@ test_cases = [
 
 @pytest.mark.parametrize('T,data,exc', test_cases)
 def test_type_check(T, data, exc):
-    @serde.serde
+    @serde.serde(type_check=Strict)
     class C:
         a: T
 
     if exc:
         with pytest.raises(serde.SerdeError):
-            d = serde.to_dict(C(data), type_check=Strict)
-            serde.from_dict(C, d, type_check=Strict)
+            d = serde.to_dict(C(data))
+            serde.from_dict(C, d)
     else:
-        d = serde.to_dict(C(data), type_check=Strict)
-        serde.from_dict(C, d, type_check=Strict)
+        d = serde.to_dict(C(data))
+        serde.from_dict(C, d)
 
 
 def test_uncoercible():
-    @serde.serde
+    @serde.serde(type_check=serde.Coerce)
     class Foo:
         i: int
 
@@ -799,14 +796,22 @@ def test_uncoercible():
 
 
 def test_coerce():
+    @serde.serde(type_check=serde.Coerce)
+    @dataclasses.dataclass
+    class Foo:
+        i: int
+        s: str
+        f: float
+        b: bool
+
     d = {"i": "10", "s": 100, "f": 1000, "b": "True"}
-    p = serde.from_dict(data.Pri, d)
+    p = serde.from_dict(Foo, d)
     assert p.i == 10
     assert p.s == "100"
     assert p.f == 1000.0
     assert p.b
 
-    p = data.Pri("10", 100, 1000, "True")
+    p = Foo("10", 100, 1000, "True")
     d = serde.to_dict(p)
     assert d["i"] == 10
     assert d["s"] == "100"
@@ -816,15 +821,43 @@ def test_coerce():
     # Couldn't coerce
     with pytest.raises(serde.SerdeError):
         d = {"i": "foo", "s": 100, "f": "bar", "b": "True"}
-        p = serde.from_dict(data.Pri, d)
+        p = serde.from_dict(Foo, d)
+
+    @serde.serde(type_check=serde.Coerce)
+    @dataclasses.dataclass
+    class Int:
+        i: int
+
+    @serde.serde(type_check=serde.Coerce)
+    @dataclasses.dataclass
+    class Str:
+        s: str
+
+    @serde.serde(type_check=serde.Coerce)
+    @dataclasses.dataclass
+    class Float:
+        f: float
+
+    @serde.serde(type_check=serde.Coerce)
+    @dataclasses.dataclass
+    class Bool:
+        b: bool
+
+    @serde.serde(type_check=serde.Coerce)
+    @serde.dataclass
+    class Nested:
+        i: data.Int
+        s: data.Str
+        f: data.Float
+        b: data.Bool
 
     # Nested structure
-    p = data.NestedPri(data.Int("10"), data.Str(100), data.Float(1000), data.Bool("True"))
+    p = Nested(Int("10"), Str(100), Float(1000), Bool("True"))
     d = serde.to_dict(p)
     assert d["i"]["i"] == 10
     assert d["s"]["s"] == "100"
     assert d["f"]["f"] == 1000.0
     assert d["b"]["b"]
 
-    d = {"i": "10", "s": 100, "f": 1000, "b": "True"}
-    p = serde.from_dict(data.Pri, d)
+    d = {"i": {"i": "10"}, "s": {"s": 100}, "f": {"f": 1000}, "b": {"b": "True"}}
+    p = serde.from_dict(Nested, d)