Repository: lovasoa/marshmallow_dataclass

Issue:
Error serialisation of Optional field and default value set is not None
```python
from dataclasses import dataclass, field
from typing import List, Optional

import marshmallow_dataclass
import marshmallow.validate


@dataclass
class Building:
    # field metadata is used to instantiate the marshmallow field
    height: float = field(metadata={"validate": marshmallow.validate.Range(min=0)})
    name: str = field(default="anonymous")
    age: Optional[int] = field(default=0)

building_schema = marshmallow_dataclass.class_schema(Building)()
building_schema.load({"name": "Eiffel Tower", "height": 324, "age": None})
```

For above code I am getting this error:-
```bash
    building_schema({"name": "Eiffel Tower", "height": 324, "age": None})
  File "/XXX/.venv/lib/python3.7/site-packages/marshmallow_dataclass/__init__.py", line 544, in load
    all_loaded = super().load(data, many=many, **kwargs)
  File "/XXX/.venv/lib/python3.7/site-packages/marshmallow/schema.py", line 724, in load
    data, many=many, partial=partial, unknown=unknown, postprocess=True
  File "/XXX/.venv/lib/python3.7/site-packages/marshmallow/schema.py", line 911, in _do_load
    raise exc
marshmallow.exceptions.ValidationError: {'age': ['Field may not be null.']}
```

Error is from marshmallow schema, but I am not able to find root cause of the error. Any help would be appreciated.

When `age: Optional[int] = field(default=None)` above code works.

Python 3.7
marshmallow_dataclass tried on 8.0.0 and 8.1.0
marshmallow 3.8.0 

Discussion:
lovasoa: This looks good ! Can you add the new `allow_none` attribute to the documentation (through the doc comments, and maybe also doctests) ?
noirbee: > This looks good ! Can you add the new `allow_none` attribute to the documentation (through the doc comments, and maybe also doctests) ?

Updated the commit with a couple of doctest strings, is this what you had in mind ?
lovasoa: Yes, this is it, thank you !
noirbee: Hi, is something else needed to get this merged ?
lovasoa: No, sorry it took time, and thanks again

Patch:
diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py
index 6367163..7da3ac2 100644
--- a/marshmallow_dataclass/__init__.py
+++ b/marshmallow_dataclass/__init__.py
@@ -297,6 +297,19 @@ def class_schema(
     Traceback (most recent call last):
     ...
     marshmallow.exceptions.ValidationError: {'name': ['Name too long']}
+
+    You can use the ``metadata`` argument to override default field behaviour, e.g. the fact that
+    ``Optional`` fields allow ``None`` values:
+
+    >>> @dataclasses.dataclass
+    ... class Custom:
+    ...     name: Optional[str] = dataclasses.field(metadata={"allow_none": False})
+    >>> class_schema(Custom)().load({"name": None})
+    Traceback (most recent call last):
+        ...
+    marshmallow.exceptions.ValidationError: {'name': ['Field may not be null.']}
+    >>> class_schema(Custom)().load({})
+    Custom(name=None)
     """
     if not dataclasses.is_dataclass(clazz):
         clazz = dataclasses.dataclass(clazz)
@@ -496,6 +509,7 @@ def field_for_schema(
             )
         elif typing_inspect.is_union_type(typ):
             if typing_inspect.is_optional_type(typ):
+                metadata["allow_none"] = metadata.get("allow_none", True)
                 metadata["default"] = metadata.get("default", None)
                 metadata["missing"] = metadata.get("missing", None)
                 metadata["required"] = False


Test patch:
diff --git a/tests/test_optional.py b/tests/test_optional.py
new file mode 100644
index 0000000..fbe9edc
--- /dev/null
+++ b/tests/test_optional.py
@@ -0,0 +1,39 @@
+import unittest
+from dataclasses import field
+from typing import Optional
+
+import marshmallow
+
+from marshmallow_dataclass import dataclass
+
+
+class TestOptionalField(unittest.TestCase):
+    def test_optional_field(self):
+        @dataclass
+        class OptionalValue:
+            value: Optional[str] = "value"
+
+        schema = OptionalValue.Schema()
+
+        self.assertEqual(schema.load({"value": None}), OptionalValue(value=None))
+        self.assertEqual(schema.load({"value": "hello"}), OptionalValue(value="hello"))
+        self.assertEqual(schema.load({}), OptionalValue())
+
+    def test_optional_field_not_none(self):
+        @dataclass
+        class OptionalValueNotNone:
+            value: Optional[str] = field(
+                default="value", metadata={"allow_none": False}
+            )
+
+        schema = OptionalValueNotNone.Schema()
+
+        self.assertEqual(schema.load({}), OptionalValueNotNone())
+        self.assertEqual(
+            schema.load({"value": "hello"}), OptionalValueNotNone(value="hello")
+        )
+        with self.assertRaises(marshmallow.exceptions.ValidationError) as exc_cm:
+            schema.load({"value": None})
+        self.assertEqual(
+            exc_cm.exception.messages, {"value": ["Field may not be null."]}
+        )