Repository: wyfo/apischema

Issue:
Add location to serialization errors
See #258 for the initial discussion.

Discussion:
wyfo: This implementation has a (significant?) impact on performance (5% slower) …
codecov-commenter: # [Codecov](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez) Report
> Merging [#298](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez) (a12d424) into [master](https://codecov.io/gh/wyfo/apischema/commit/b68fa0b45c2efd49517bad0475244ea69e498a28?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez) (b68fa0b) will **decrease** coverage by `0.10%`.
> The diff coverage is `78.37%`.

[![Impacted file tree graph](https://codecov.io/gh/wyfo/apischema/pull/298/graphs/tree.svg?width=650&height=150&src=pr&token=GTGPD4DLE5&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez)](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez)

```diff
@@            Coverage Diff             @@
##           master     #298      +/-   ##
==========================================
- Coverage   85.55%   85.44%   -0.11%     
==========================================
  Files          65       66       +1     
  Lines        5654     5675      +21     
  Branches     1167     1170       +3     
==========================================
+ Hits         4837     4849      +12     
- Misses        598      607       +9     
  Partials      219      219              
```


| [Impacted Files](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez) | Coverage Δ | |
|---|---|---|
| [apischema/serialization/methods.py](https://codecov.io/gh/wyfo/apischema/pull/298/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez#diff-YXBpc2NoZW1hL3NlcmlhbGl6YXRpb24vbWV0aG9kcy5weQ==) | `86.09% <77.61%> (-2.41%)` | :arrow_down: |
| [apischema/serialization/errors.py](https://codecov.io/gh/wyfo/apischema/pull/298/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez#diff-YXBpc2NoZW1hL3NlcmlhbGl6YXRpb24vZXJyb3JzLnB5) | `85.71% <85.71%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez). Last update [15683c0...a12d424](https://codecov.io/gh/wyfo/apischema/pull/298?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joseph+Perez).


Patch:
diff --git a/apischema/deserialization/__init__.py b/apischema/deserialization/__init__.py
index 4b04ad4..d19f611 100644
--- a/apischema/deserialization/__init__.py
+++ b/apischema/deserialization/__init__.py
@@ -224,20 +224,20 @@ class DeserializationMethodVisitor(
         self,
         additional_properties: bool,
         aliaser: Aliaser,
-        allowed_types: CollectionOrPredicate[type],
         coercer: Optional[Coercer],
         default_conversion: DefaultConversion,
         fall_back_on_default: bool,
         no_copy: bool,
+        pass_through: CollectionOrPredicate[type],
     ):
         super().__init__(default_conversion)
         self.additional_properties = additional_properties
         self.aliaser = aliaser
-        self.allowed_types = allowed_types
-        self.allow_type = as_predicate(allowed_types)
         self.coercer = coercer
         self.fall_back_on_default = fall_back_on_default
         self.no_copy = no_copy
+        self.pass_through = pass_through
+        self.pass_through_type = as_predicate(pass_through)
 
     def _recursive_result(
         self, lazy: Lazy[DeserializationMethodFactory]
@@ -254,12 +254,12 @@ class DeserializationMethodVisitor(
             tp,
             self.additional_properties,
             self.aliaser,
-            self.allowed_types,
             self.coercer,
             self._conversion,
             self.default_conversion,
             self.fall_back_on_default,
             self.no_copy,
+            self.pass_through,
         )
 
     def annotated(
@@ -543,7 +543,7 @@ class DeserializationMethodVisitor(
             method = SubprimitiveMethod(
                 cls, primitive_factory.merge(constraints, validators).method
             )
-            if self.allow_type(cls):
+            if self.pass_through_type(cls):
                 return TypeCheckMethod(cls, method)
             return method
 
@@ -650,7 +650,7 @@ class DeserializationMethodVisitor(
         if (
             is_type(cls)  # check for type first in order to have it hashable
             and cls not in JSON_TYPES  # eliminate most common types
-            and self.allow_type(cls)
+            and self.pass_through_type(cls)
             and not is_typed_dict(cls)  # typed dict isinstance cannot be checked
         ):
 
@@ -668,21 +668,21 @@ def deserialization_method_factory(
     tp: AnyType,
     additional_properties: bool,
     aliaser: Aliaser,
-    allowed_types: CollectionOrPredicate[type],
     coercer: Optional[Coercer],
     conversion: Optional[AnyConversion],
     default_conversion: DefaultConversion,
     fall_back_on_default: bool,
     no_copy: bool,
+    pass_through: CollectionOrPredicate[type],
 ) -> DeserializationMethodFactory:
     return DeserializationMethodVisitor(
         additional_properties,
         aliaser,
-        allowed_types,
         coercer,
         default_conversion,
         fall_back_on_default,
         no_copy,
+        pass_through,
     ).visit_with_conv(tp, conversion)
 
 
@@ -692,12 +692,12 @@ def deserialization_method(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> Callable[[Any], T]:
@@ -710,12 +710,12 @@ def deserialization_method(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> Callable[[Any], Any]:
@@ -727,12 +727,12 @@ def deserialization_method(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> Callable[[Any], Any]:
@@ -743,20 +743,20 @@ def deserialization_method(
         coercer = coerce
     elif opt_or(coerce, settings.deserialization.coerce):
         coercer = settings.deserialization.coercer
-    allowed_types = opt_or(allowed_types, settings.deserialization.allowed_types)
-    if isinstance(allowed_types, Collection) and not isinstance(allowed_types, tuple):
-        allowed_types = tuple(allowed_types)
+    pass_through = opt_or(pass_through, settings.deserialization.pass_through)
+    if isinstance(pass_through, Collection) and not isinstance(pass_through, tuple):
+        pass_through = tuple(pass_through)
     return (
         deserialization_method_factory(
             type,
             opt_or(additional_properties, settings.additional_properties),
             opt_or(aliaser, settings.aliaser),
-            allowed_types,  # type: ignore
             coercer,
             conversion,
             opt_or(default_conversion, settings.deserialization.default_conversion),
             opt_or(fall_back_on_default, settings.deserialization.fall_back_on_default),
             opt_or(no_copy, settings.deserialization.no_copy),
+            pass_through,  # type: ignore
         )
         .merge(get_constraints(schema), tuple(map(Validator, validators)))
         .method.deserialize
@@ -770,12 +770,12 @@ def deserialize(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> T:
@@ -789,12 +789,12 @@ def deserialize(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> Any:
@@ -814,12 +814,12 @@ def deserialize(
     *,
     additional_properties: bool = None,
     aliaser: Aliaser = None,
-    allowed_types: CollectionOrPredicate[type] = None,
     coerce: Coerce = None,
     conversion: AnyConversion = None,
     default_conversion: DefaultConversion = None,
     fall_back_on_default: bool = None,
     no_copy: bool = None,
+    pass_through: CollectionOrPredicate[type] = None,
     schema: Schema = None,
     validators: Collection[Callable] = ()
 ) -> Any:
@@ -827,12 +827,12 @@ def deserialize(
         type,
         additional_properties=additional_properties,
         aliaser=aliaser,
-        allowed_types=allowed_types,
         coerce=coerce,
         conversion=conversion,
         default_conversion=default_conversion,
         fall_back_on_default=fall_back_on_default,
         no_copy=no_copy,
+        pass_through=pass_through,
         schema=schema,
         validators=validators,
     )(data)
diff --git a/apischema/serialization/errors.py b/apischema/serialization/errors.py
new file mode 100644
index 0000000..c2374ac
--- /dev/null
+++ b/apischema/serialization/errors.py
@@ -0,0 +1,10 @@
+from typing import Sequence, Union
+
+
+class TypeCheckError(TypeError):
+    def __init__(self, msg: str, loc: Sequence[Union[int, str]]):
+        self.msg = msg
+        self.loc = loc
+
+    def __str__(self):
+        return f"{list(self.loc)} {self.msg}"
diff --git a/apischema/serialization/methods.py b/apischema/serialization/methods.py
index 6a61c5e..1cca82c 100644
--- a/apischema/serialization/methods.py
+++ b/apischema/serialization/methods.py
@@ -1,48 +1,55 @@
 from dataclasses import dataclass, field
-from typing import AbstractSet, Any, Callable, Optional, Tuple
+from typing import AbstractSet, Any, Callable, Optional, Tuple, Union
 
 from apischema.conversions.utils import Converter
 from apischema.fields import FIELDS_SET_ATTR
+from apischema.serialization.errors import TypeCheckError
 from apischema.types import AnyType, Undefined
 from apischema.utils import Lazy
 
 
 class SerializationMethod:
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         raise NotImplementedError
 
 
 class IdentityMethod(SerializationMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return obj
 
 
 class ListMethod(SerializationMethod):
-    serialize = staticmethod(list)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return list(obj)
 
 
 class DictMethod(SerializationMethod):
-    serialize = staticmethod(dict)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return dict(obj)
 
 
 class StrMethod(SerializationMethod):
-    serialize = staticmethod(str)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return str(obj)
 
 
 class IntMethod(SerializationMethod):
-    serialize = staticmethod(int)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return int(obj)
 
 
 class BoolMethod(SerializationMethod):
-    serialize = staticmethod(bool)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return bool(obj)
 
 
 class FloatMethod(SerializationMethod):
-    serialize = staticmethod(float)  # type: ignore
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return float(obj)
 
 
 class NoneMethod(SerializationMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return None
 
 
@@ -54,7 +61,7 @@ class RecMethod(SerializationMethod):
     def __post_init__(self):
         self.method = None
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         if self.method is None:
             self.method = self.lazy()
         return self.method.serialize(obj)
@@ -64,13 +71,13 @@ class RecMethod(SerializationMethod):
 class AnyMethod(SerializationMethod):
     factory: Callable[[AnyType], SerializationMethod]
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         method = self.factory(obj.__class__)  # tmp  variable for substitution
-        return method.serialize(obj)
+        return method.serialize(obj, path)
 
 
 class Fallback:
-    def fall_back(self, obj: Any) -> Any:
+    def fall_back(self, obj: Any, path: Union[int, str, None]) -> Any:
         raise NotImplementedError
 
 
@@ -78,16 +85,19 @@ class Fallback:
 class NoFallback(Fallback):
     tp: AnyType
 
-    def fall_back(self, obj: Any) -> Any:
-        raise TypeError(f"Expected {self.tp}, found {obj.__class__}")
+    def fall_back(self, obj: Any, path: Union[int, str, None]) -> Any:
+        raise TypeCheckError(
+            f"Expected {self.tp}, found {obj.__class__}",
+            [path] if path is not None else [],
+        )
 
 
 @dataclass
 class AnyFallback(Fallback):
     any_method: SerializationMethod
 
-    def fall_back(self, obj: Any) -> Any:
-        return self.any_method.serialize(obj)
+    def fall_back(self, obj: Any, key: Union[int, str, None]) -> Any:
+        return self.any_method.serialize(obj, key)
 
 
 @dataclass
@@ -95,8 +105,12 @@ class TypeCheckIdentityMethod(SerializationMethod):
     expected: AnyType  # `type` would require exact match (i.e. no EnumMeta)
     fallback: Fallback
 
-    def serialize(self, obj: Any) -> Any:
-        return obj if isinstance(obj, self.expected) else self.fallback.fall_back(obj)
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return (
+            obj
+            if isinstance(obj, self.expected)
+            else self.fallback.fall_back(obj, path)
+        )
 
 
 @dataclass
@@ -105,21 +119,25 @@ class TypeCheckMethod(SerializationMethod):
     expected: AnyType  # `type` would require exact match (i.e. no EnumMeta)
     fallback: Fallback
 
-    def serialize(self, obj: Any) -> Any:
-        return (
-            self.method.serialize(obj)
-            if isinstance(obj, self.expected)
-            else self.fallback.fall_back(obj)
-        )
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        if isinstance(obj, self.expected):
+            try:
+                return self.method.serialize(obj)
+            except TypeCheckError as err:
+                if path is None:
+                    raise
+                raise TypeCheckError(err.msg, [path, *err.loc])
+        else:
+            return self.fallback.fall_back(obj, path)
 
 
 @dataclass
 class CollectionCheckOnlyMethod(SerializationMethod):
     value_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
-        for elt in obj:
-            self.value_method.serialize(elt)
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        for i, elt in enumerate(obj):
+            self.value_method.serialize(elt, i)
         return obj
 
 
@@ -127,12 +145,12 @@ class CollectionCheckOnlyMethod(SerializationMethod):
 class CollectionMethod(SerializationMethod):
     value_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
-        return [self.value_method.serialize(elt) for elt in obj]
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return [self.value_method.serialize(elt, i) for i, elt in enumerate(obj)]
 
 
 class ValueMethod(SerializationMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return obj.value
 
 
@@ -140,7 +158,7 @@ class ValueMethod(SerializationMethod):
 class EnumMethod(SerializationMethod):
     any_method: AnyMethod
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return self.any_method.serialize(obj.value)
 
 
@@ -149,10 +167,10 @@ class MappingCheckOnlyMethod(SerializationMethod):
     key_method: SerializationMethod
     value_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         for key, value in obj.items():
-            self.key_method.serialize(key)
-            self.value_method.serialize(value)
+            self.key_method.serialize(key, key)
+            self.value_method.serialize(value, key)
         return obj
 
 
@@ -161,9 +179,9 @@ class MappingMethod(SerializationMethod):
     key_method: SerializationMethod
     value_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return {
-            self.key_method.serialize(key): self.value_method.serialize(value)
+            self.key_method.serialize(key, key): self.value_method.serialize(value, key)
             for key, value in obj.items()
         }
 
@@ -210,7 +228,7 @@ class SimpleField(IdentityField):
     ):
         if serialize_field(self, obj, typed_dict, exclude_unset):
             result[self.alias] = self.method.serialize(
-                get_field_value(self, obj, typed_dict)
+                get_field_value(self, obj, typed_dict), self.alias
             )
 
 
@@ -240,9 +258,9 @@ class ComplexField(SimpleField):
                 or (self.skip_default and value == self.default_value)
             ):
                 if self.alias is not None:
-                    result[self.alias] = self.method.serialize(value)
+                    result[self.alias] = self.method.serialize(value, self.alias)
                 else:
-                    result.update(self.method.serialize(value))
+                    result.update(self.method.serialize(value, self.alias))
 
 
 @dataclass
@@ -260,7 +278,7 @@ class SerializedField(BaseField):
         if not (self.undefined and value is Undefined) and not (
             self.skip_none and value is None
         ):
-            result[self.alias] = self.method.serialize(value)
+            result[self.alias] = self.method.serialize(value, self.alias)
 
 
 @dataclass
@@ -270,7 +288,7 @@ class ObjectMethod(SerializationMethod):
 
 @dataclass
 class ClassMethod(ObjectMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         result: dict = {}
         for i in range(len(self.fields)):
             field: BaseField = self.fields[i]
@@ -280,7 +298,7 @@ class ClassMethod(ObjectMethod):
 
 @dataclass
 class ClassWithFieldsSetMethod(ObjectMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         result: dict = {}
         for i in range(len(self.fields)):
             field: BaseField = self.fields[i]
@@ -290,7 +308,7 @@ class ClassWithFieldsSetMethod(ObjectMethod):
 
 @dataclass
 class TypedDictMethod(ObjectMethod):
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         result: dict = {}
         for i in range(len(self.fields)):
             field: BaseField = self.fields[i]
@@ -303,11 +321,11 @@ class TypedDictWithAdditionalMethod(TypedDictMethod):
     field_names: AbstractSet[str]
     any_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         result: dict = super().serialize(obj)
         for key, value in obj.items():
             if key not in self.field_names and isinstance(key, str):
-                result[str(key)] = self.any_method.serialize(value)
+                result[str(key)] = self.any_method.serialize(value, key)
         return result
 
 
@@ -315,10 +333,10 @@ class TypedDictWithAdditionalMethod(TypedDictMethod):
 class TupleCheckOnlyMethod(SerializationMethod):
     elt_methods: Tuple[SerializationMethod, ...]
 
-    def serialize(self, obj: tuple) -> Any:
+    def serialize(self, obj: tuple, path: Union[int, str, None] = None) -> Any:
         for i in range(len(self.elt_methods)):
             method: SerializationMethod = self.elt_methods[i]
-            method.serialize(obj[i])
+            method.serialize(obj[i], i)
         return obj
 
 
@@ -326,11 +344,11 @@ class TupleCheckOnlyMethod(SerializationMethod):
 class TupleMethod(SerializationMethod):
     elt_methods: Tuple[SerializationMethod, ...]
 
-    def serialize(self, obj: tuple) -> Any:
+    def serialize(self, obj: tuple, path: Union[int, str, None] = None) -> Any:
         elts: list = [None] * len(self.elt_methods)
         for i in range(len(self.elt_methods)):
             method: SerializationMethod = self.elt_methods[i]
-            elts[i] = method.serialize(obj[i])
+            elts[i] = method.serialize(obj[i], i)
         return elts
 
 
@@ -339,7 +357,7 @@ class CheckedTupleMethod(SerializationMethod):
     nb_elts: int
     method: SerializationMethod
 
-    def serialize(self, obj: tuple) -> Any:
+    def serialize(self, obj: tuple, path: Union[int, str, None] = None) -> Any:
         if not len(obj) == self.nb_elts:
             raise TypeError(f"Expected {self.nb_elts}-tuple, found {len(obj)}-tuple")
         return self.method.serialize(obj)
@@ -353,8 +371,8 @@ class CheckedTupleMethod(SerializationMethod):
 class OptionalMethod(SerializationMethod):
     value_method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
-        return self.value_method.serialize(obj) if obj is not None else None
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
+        return self.value_method.serialize(obj, path) if obj is not None else None
 
 
 @dataclass
@@ -374,22 +392,22 @@ class UnionMethod(SerializationMethod):
     alternatives: Tuple[UnionAlternative, ...]
     fallback: Fallback
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         for i in range(len(self.alternatives)):
             alternative: UnionAlternative = self.alternatives[i]
             if isinstance(obj, alternative.cls):
                 try:
-                    return alternative.method.serialize(obj)
+                    return alternative.method.serialize(obj, path)
                 except Exception:
                     pass
-        self.fallback.fall_back(obj)
+        self.fallback.fall_back(obj, path)
 
 
 @dataclass
 class WrapperMethod(SerializationMethod):
     wrapped: Callable[[Any], Any]
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return self.wrapped(obj)
 
 
@@ -398,5 +416,5 @@ class ConversionMethod(SerializationMethod):
     converter: Converter
     method: SerializationMethod
 
-    def serialize(self, obj: Any) -> Any:
+    def serialize(self, obj: Any, path: Union[int, str, None] = None) -> Any:
         return self.method.serialize(self.converter(obj))
diff --git a/apischema/settings.py b/apischema/settings.py
index 7fc2eab..6fd4b5e 100644
--- a/apischema/settings.py
+++ b/apischema/settings.py
@@ -108,13 +108,13 @@ class settings(metaclass=MetaSettings):
         missing_property: str = "missing property"
 
     class deserialization(metaclass=ResetCache):
-        allowed_types: CollectionOrPredicate[type] = ()
         coerce: bool = False
         coercer: Coercer = coerce_
         default_conversion: DefaultConversion = default_deserialization
         fall_back_on_default: bool = False
         no_copy: bool = True
         override_dataclass_constructors = False
+        pass_through: CollectionOrPredicate[type] = ()
 
     class serialization(metaclass=ResetCache):
         check_type: bool = False
diff --git a/docs/de_serialization.md b/docs/de_serialization.md
index 6094bdc..cfa379a 100644
--- a/docs/de_serialization.md
+++ b/docs/de_serialization.md
@@ -17,17 +17,20 @@ Let's start again with the [overview example](index.md#example)
 
 Deserialization performs a validation of data, based on typing annotations and other information (see [schema](json_schema.md) and [validation](validation.md)).
 
-### Allowed types
+### Deserialization passthrough
 
 In some case, e.g. MessagePack loading with raw bytes inside, some data will have other type than
-JSON primitive ones. These types can be allowed using `allowed_types` parameter; it must be collection of classes, or a predicate.
+JSON primitive ones. These types can be allowed using `pass_through` parameter; it must be collection of classes, or a predicate. Behavior can also be set globally using `apischema.settings.deserialization.pass_through`.
 
 Only non JSON primitive classes can be allowed, because *apischema* relies on a type check with `isinstance` to skip deserialization. That exclude `NewType` but also `TypeDict`. 
 
 ```python
-{!allowed_types.py!}
+{!deserialization_pass_through.py!}
 ```
 
+!!! note
+    Equivalent serialization feature is presented in [optimizations documentation](performance_and_benchmark.md#serialization-passthrough).
+
 ### Strictness
 
 #### Coercion
diff --git a/examples/allowed_types.py b/examples/deserialization_pass_through.py
similarity index 62%
rename from examples/allowed_types.py
rename to examples/deserialization_pass_through.py
index 30f527c..8e1bc68 100644
--- a/examples/allowed_types.py
+++ b/examples/deserialization_pass_through.py
@@ -4,11 +4,11 @@ from apischema import deserialize
 
 start, end = datetime.now(), datetime.now() + timedelta(1)
 assert deserialize(
-    tuple[datetime, datetime], [start, end], allowed_types={datetime}
+    tuple[datetime, datetime], [start, end], pass_through={datetime}
 ) == (start, end)
-# allowed types can also be deserialized normally from JSON types
+# Passing through types can also be deserialized normally from JSON types
 assert deserialize(
     tuple[datetime, datetime],
     [start.isoformat(), end.isoformat()],
-    allowed_types={datetime},
+    pass_through={datetime},
 ) == (start, end)
diff --git a/scripts/cythonize.py b/scripts/cythonize.py
index d2a1210..b658a05 100755
--- a/scripts/cythonize.py
+++ b/scripts/cythonize.py
@@ -91,7 +91,6 @@ def cython_signature(
     def_type: CythonDef, func: FunctionType, self_type: Optional[type] = None
 ) -> str:
     parameters = list(inspect.signature(func).parameters.values())
-    assert all(p.default is inspect.Parameter.empty for p in parameters)
     types = get_type_hints(func)
     param_with_types = []
     if parameters[0].name == "self":
@@ -102,7 +101,10 @@ def cython_signature(
             parameters.pop(0)
     for param in parameters:
         param_type = cython_type(types[param.name], func.__module__)
-        param_with_types.append(f"{param_type} {param.name}")
+        assert param.default is inspect.Parameter.empty or param.default is None
+        param_with_types.append(
+            f"{param_type} {param.name}" + (" = None" if param.default is None else "")
+        )
     func_name = method_name(self_type, func.__name__) if self_type else func.__name__
     return f"{def_type} {func_name}(" + ", ".join(param_with_types) + "):"
 
@@ -230,7 +232,7 @@ def write_class(pyx: IndentedWriter, cls: type):
                 if (
                     not name.startswith("_")
                     and name not in annotations
-                    and isinstance(obj, (FunctionType, staticmethod))
+                    and isinstance(obj, FunctionType)
                 ):
                     pyx.writeln()
                     base_method = getattr(base_class, name)
@@ -265,16 +267,8 @@ def write_methods(pyx: IndentedWriter, method: Method):
     for cls, dispatch in get_dispatch(method.base_class).items():
         if method.name in cls.__dict__:
             sub_method = cls.__dict__[method.name]
-            if isinstance(sub_method, staticmethod):
-                with pyx.write_block(
-                    cython_signature("cdef inline", method.function, cls)  # type: ignore
-                ):
-                    _, param = inspect.signature(method.function).parameters
-                    func = sub_method.__get__(None, object)
-                    pyx.writeln(f"return {func.__name__}({param})")
-            else:
-                with pyx.write_block(cython_signature("cdef inline", sub_method, cls)):
-                    pyx.writelines(get_body(sub_method, cls))
+            with pyx.write_block(cython_signature("cdef inline", sub_method, cls)):
+                pyx.writelines(get_body(sub_method, cls))
             pyx.writeln()
 
 


Test patch:
diff --git a/tests/integration/test_allowed_types.py b/tests/integration/test_deserialization_pass_through.py
similarity index 74%
rename from tests/integration/test_allowed_types.py
rename to tests/integration/test_deserialization_pass_through.py
index f0cfb2c..b6ba306 100644
--- a/tests/integration/test_allowed_types.py
+++ b/tests/integration/test_deserialization_pass_through.py
@@ -15,9 +15,9 @@ valid_bytes = b"toto" + (sum(b"toto") % 255).to_bytes(1, byteorder=sys.byteorder
 invalid_bytes = b"toto" + (sum(b"toto") % 255 + 42).to_bytes(1, byteorder=sys.byteorder)
 
 
-def test_allowed_types_run_upper_validators():
+def test_pass_through_run_upper_validators():
     method = deserialization_method(
-        bytes, allowed_types={bytes}, validators=[validate_checksum]
+        bytes, pass_through={bytes}, validators=[validate_checksum]
     )
     assert method(valid_bytes) is valid_bytes
     with pytest.raises(ValidationError):
@@ -34,10 +34,10 @@ class MyClass:
             raise ValidationError("ZERO!")
 
 
-def test_allowed_types_doesnt_run_type_validators():
+def test_pass_through_doesnt_run_type_validators():
     obj = MyClass(0)
-    method = deserialization_method(MyClass, allowed_types={MyClass})
+    method = deserialization_method(MyClass, pass_through={MyClass})
     assert method(obj) is obj
     with pytest.raises(ValidationError):
         method({"fields": 0})
-        deserialize(MyClass, {"field": 0}, allowed_types={MyClass})
+        deserialize(MyClass, {"field": 0}, pass_through={MyClass})