Repository: lovasoa/marshmallow_dataclass

Issue:
Use NewType for derived List class
Hi there!

Coming from vanilla marshmallow, I had a `List` class that was deriving from `marshmallow.fields.List` and was adding some logic to how it was reading the input data on `_deserialize`.

Now, I am trying to integrate this with `marshmallow_dataclass` and hence create a new type. I have created a few other types that were not lists and it worked well, but I can't get it to work for lists. Here is what I have tried so far:

```python3
import typing

import marshmallow
from marshmallow_dataclass import dataclass, NewType


class MyList(marshmallow.fields.List):
    """A list field type that properly handles lists in MultiDict"""

    def _deserialize(self, value, attr, data, **kwargs):
        # removed the code, just checking if this is called
        raise Exception("Finally Called")


ListType = NewType("ListType", typing.List, field=MyList)


@dataclass 
class TestDataClass1:
    """
    This is the one I was expecting would work, as I thought that it would pass
    `int` to `MyList` which would come back to something similar to vanilla
    marshmallow i.e. `values = MyList(Int())`
    """
    values: ListType(int)

# fails with 'Not a valid integer.'
TestDataClass1.Schema().load({"values": [1,2,3]})

# Second try!
@dataclass 
class TestDataClass2: 
    values: ListType(typing.List[int])

# list is properly loaded, but that means MyList._deserialize was not called
TestDataClass2.Schema().load({"values": [1,2,3]})
```

I looked in the doc and example but could not find a reference for this use case, could you help me figure it out?


Discussion:
YBadiss: Very useful to us, and it fixes my issue #66 ! 👍 
lovasoa: Hello ! I like the idea of being able to handle custom serialization of collections. However, I don't really like the overhead the new parameter adds. Do you think we could use the existing `base_schema.TYPE_MAPPING` for this ?
lovasoa: Great ! Let's just rename `generic_type_mapping` to `type_mapping` (since it does not contain only generics), and we'll merge.

Patch:
diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py
index 9b223a5..d6f1554 100644
--- a/marshmallow_dataclass/__init__.py
+++ b/marshmallow_dataclass/__init__.py
@@ -351,6 +351,7 @@ def field_for_schema(
     """
 
     metadata = {} if metadata is None else dict(metadata)
+
     if default is not marshmallow.missing:
         metadata.setdefault("default", default)
         # 'missing' must not be set for required fields.
@@ -383,16 +384,22 @@ def field_for_schema(
     origin = typing_inspect.get_origin(typ)
     if origin:
         arguments = typing_inspect.get_args(typ, True)
+        # Override base_schema.TYPE_MAPPING to change the class used for generic types below
+        type_mapping = base_schema.TYPE_MAPPING if base_schema else {}
+
         if origin in (list, List):
             child_type = field_for_schema(arguments[0], base_schema=base_schema)
-            return marshmallow.fields.List(child_type, **metadata)
+            list_type = type_mapping.get(List, marshmallow.fields.List)
+            return list_type(child_type, **metadata)
         if origin in (tuple, Tuple):
             children = tuple(
                 field_for_schema(arg, base_schema=base_schema) for arg in arguments
             )
-            return marshmallow.fields.Tuple(children, **metadata)
+            tuple_type = type_mapping.get(Tuple, marshmallow.fields.Tuple)
+            return tuple_type(children, **metadata)
         elif origin in (dict, Dict):
-            return marshmallow.fields.Dict(
+            dict_type = type_mapping.get(Dict, marshmallow.fields.Dict)
+            return dict_type(
                 keys=field_for_schema(arguments[0], base_schema=base_schema),
                 values=field_for_schema(arguments[1], base_schema=base_schema),
                 **metadata,


Test patch:
diff --git a/tests/test_field_for_schema.py b/tests/test_field_for_schema.py
index c597e87..699bd55 100644
--- a/tests/test_field_for_schema.py
+++ b/tests/test_field_for_schema.py
@@ -2,7 +2,7 @@ import inspect
 import typing
 import unittest
 from enum import Enum
-from typing import Dict, Optional, Union, Any
+from typing import Dict, Optional, Union, Any, List, Tuple
 
 from marshmallow import fields, Schema
 
@@ -117,6 +117,26 @@ class TestFieldForSchema(unittest.TestCase):
             fields.Nested(NewDataclass.Schema),
         )
 
+    def test_override_container_type_with_type_mapping(self):
+        type_mapping = [
+            (List, fields.List, List[int]),
+            (Dict, fields.Dict, Dict[str, int]),
+            (Tuple, fields.Tuple, Tuple[int, str, bytes]),
+        ]
+        for base_type, marshmallow_field, schema in type_mapping:
+
+            class MyType(marshmallow_field):
+                ...
+
+            self.assertIsInstance(field_for_schema(schema), marshmallow_field)
+
+            class BaseSchema(Schema):
+                TYPE_MAPPING = {base_type: MyType}
+
+            self.assertIsInstance(
+                field_for_schema(schema, base_schema=BaseSchema), MyType
+            )
+
 
 if __name__ == "__main__":
     unittest.main()