Repository: lidatong/dataclasses-json

Issue:
field_name is not supported with schema
```ipython
In [4]: @dataclasses_json.dataclass_json
   ...: @dataclasses.dataclass
   ...: class Person:
   ...:     name: str = dataclasses.field(metadata=dataclasses_json.config(field_name="fullname"))
   ...:     age: int
   ...:     

In [5]: p = Person("A B", 23)

In [6]: p
Out[6]: Person(name='A B', age=23)

In [7]: p.to_dict()
Out[7]: {'fullname': 'A B', 'age': 23}

In [8]: d = p.to_dict()

In [9]: Person.from_dict(d)
Out[9]: Person(name='A B', age=23)

In [10]: Person.schema().load(d)
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
<ipython-input-10-cd95d439489f> in <module>()
----> 1 Person.schema().load(d)

/home/venv/lib/python3.7/site-packages/marshmallow/schema.py in load(self, data, many, partial, unknown)
    705         return self._do_load(
    706             data, many, partial=partial, unknown=unknown,
--> 707             postprocess=True,
    708         )
    709 

/home/venv/lib/python3.7/site-packages/marshmallow/schema.py in _do_load(self, data, many, partial, unknown, postprocess)
    865             )
    866             self.handle_error(exc, data)
--> 867             raise exc
    868 
    869         return result

ValidationError: {'fullname': ['Unknown field.']}
```

It seems that the schema should use `data_key` from the [marshmallow Field](https://marshmallow.readthedocs.io/en/3.0/api_reference.html#module-marshmallow.fields)

Discussion:

Patch:
diff --git a/dataclasses_json/mm.py b/dataclasses_json/mm.py
index 08a69e9..5df24eb 100644
--- a/dataclasses_json/mm.py
+++ b/dataclasses_json/mm.py
@@ -1,6 +1,7 @@
 import typing
 import warnings
 import sys
+from copy import deepcopy
 
 from dataclasses import MISSING, is_dataclass, fields as dc_fields
 from datetime import datetime
@@ -14,7 +15,7 @@ from marshmallow import fields, Schema, post_load
 from marshmallow_enum import EnumField
 
 from dataclasses_json.core import (_is_supported_generic, _decode_dataclass,
-                                   _ExtendedEncoder)
+                                   _ExtendedEncoder, _user_overrides)
 from dataclasses_json.utils import (_is_collection, _is_optional,
                                     _issubclass_safe, _timestamp_to_dt_aware,
                                     _is_new_type, _get_type_origin)
@@ -64,22 +65,23 @@ class _UnionField(fields.Field):
         return super()._serialize(value, attr, obj, **kwargs)
 
     def _deserialize(self, value, attr, data, **kwargs):
-        if isinstance(value, dict) and '__type' in value:
-            dc_name = value['__type']
+        tmp_value = deepcopy(value)
+        if isinstance(tmp_value, dict) and '__type' in tmp_value:
+            dc_name = tmp_value['__type']
             for type_, schema_ in self.desc.items():
                 if is_dataclass(type_) and type_.__name__ == dc_name:
-                    del value['__type']
-                    return schema_._deserialize(value, attr, data, **kwargs)
+                    del tmp_value['__type']
+                    return schema_._deserialize(tmp_value, attr, data, **kwargs)
         for type_, schema_ in self.desc.items():
-            if isinstance(value, _get_type_origin(type_)):
-                return schema_._deserialize(value, attr, data, **kwargs)
+            if isinstance(tmp_value, _get_type_origin(type_)):
+                return schema_._deserialize(tmp_value, attr, data, **kwargs)
         else:
             warnings.warn(
-                f'The type "{type(value).__name__}" (value: "{value}") '
+                f'The type "{type(tmp_value).__name__}" (value: "{tmp_value}") '
                 f'is not in the list of possible types of typing.Union '
                 f'(dataclass: {self.cls.__name__}, field: {self.field.name}). '
                 f'Value cannot be deserialized properly.')
-        return super()._deserialize(value, attr, data, **kwargs)
+        return super()._deserialize(tmp_value, attr, data, **kwargs)
 
 
 TYPES = {
@@ -236,10 +238,12 @@ def build_type(type_, options, mixin, field, cls):
 
 def schema(cls, mixin, infer_missing):
     schema = {}
+    overrides = _user_overrides(cls)
     for field in dc_fields(cls):
         metadata = (field.metadata or {}).get('dataclasses_json', {})
-        if 'mm_field' in metadata:
-            schema[field.name] = metadata['mm_field']
+        metadata = overrides[field.name]
+        if metadata.mm_field is not None:
+            schema[field.name] = metadata.mm_field
         else:
             type_ = field.type
             options = {}
@@ -259,6 +263,9 @@ def schema(cls, mixin, infer_missing):
                     # Union[str, int, None] is optional too, but it has more than 1 typed field.
                     type_ = type_.__args__[0]
 
+            if metadata.letter_case is not None:
+                options['data_key'] = metadata.letter_case(field.name)
+
             t = build_type(type_, options, mixin, field, cls)
             # if type(t) is not fields.Field:  # If we use `isinstance` we would return nothing.
             schema[field.name] = t


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 905e3b1..e565242 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,4 +1,5 @@
 from .entities import DataClassDefaultListStr, DataClassDefaultOptionalList, DataClassList, DataClassOptional
+from .test_letter_case import CamelCasePerson, KebabCasePerson, SnakeCasePerson, FieldNamePerson
 
 
 test_do_list = """[{}, {"children": [{"name": "a"}, {"name": "b"}]}]"""
@@ -21,3 +22,8 @@ class TestSchema:
     def test_optional(self):
         DataClassOptional.schema().loads('{"a": 4, "b": null}')
         assert True
+
+    def test_letter_case(self):
+        for cls in (CamelCasePerson, KebabCasePerson, SnakeCasePerson, FieldNamePerson):
+            p = cls('Alice')
+            assert p.to_dict() == cls.schema().dump(p)
diff --git a/tests/test_union.py b/tests/test_union.py
index 5f90ff2..c2e8650 100644
--- a/tests/test_union.py
+++ b/tests/test_union.py
@@ -125,6 +125,16 @@ def test_deserialize(expected_obj, data, data_json):
     assert s.loads(data_json) == expected_obj
 
 
+def test_deserialize_twice():
+    data = {"f1": [{"f1": 12, "__type": "Aux1"}, {"f1": "str3", "__type": "Aux2"}]}
+    expected_obj = C9([Aux1(12), Aux2("str3")])
+
+    s = C9.schema()
+    res1 = s.load(data)
+    res2 = s.load(data)
+    assert res1 == expected_obj and res2 == expected_obj
+
+
 @pytest.mark.parametrize('obj', [
     (C2(f1={"str1": "str1"})),
     (C3(f1=[0.12, 0.13, "str1"])),