Repository: marshmallow-code/marshmallow

Issue:
Regression on list of nullable nested fields
Hello,

While porting some code from 2.20 to 3.3, I stumbled upon this change in behavior regarding list of nullable fields.

in 2.20 the following code works:
```python
from marshmallow import fields, Schema

class S(Schema):
    f = fields.List(fields.Nested(Schema, allow_none=True))

assert S().dump(dict(f=[None])).data == dict(f=[None])
```
whereas the same code in 3.3 fails with
`AssertionError: assert {'f': [{}]} == {'f': [None]}`
```python
from marshmallow import fields, Schema

class S(Schema):
    f = fields.List(fields.Nested(Schema, allow_none=True))

assert S().dump(dict(f=[None])) == dict(f=[None])
```

I could not find anything related to this in the upgrade instructions or the documentation.
I'm fully aware that this is an edge case and I don't mind looking into it if it's actually considered as a bug.

here are two unit tests that can be used to reproduce this issue:
```python
def test_list_of_nested_nullable_serialize_none_to_none(self):
    res = fields.List(fields.Nested(Schema, allow_none=True)).serialize("foo", {"foo": [None]})
    assert res == [None]

def test_nested_multiple_nullable_serialize_none_to_none(self):
    res = fields.Nested(Schema, allow_none=True, many=True).serialize("foo", {"foo": [None]})
    assert res == [None]
```

Discussion:
sloria: Sorry, I haven't been following the related issues. On a cursory glance this seems like the right course, but it's unfortunate that it will introduce a performance regression. I defer to your and @lafrech 's judgment for merging and releasing this.

Patch:
diff --git a/setup.py b/setup.py
index 370b0467..951594ca 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@ EXTRAS_REQUIRE = {
         "pre-commit>=1.20,<3.0",
     ],
     "docs": [
-        "sphinx==2.4.0",
+        "sphinx==2.3.1",
         "sphinx-issues==1.2.0",
         "alabaster==0.7.12",
         "sphinx-version-warning==1.1.2",
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index e0575d65..27c6740a 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -146,7 +146,8 @@ class Field(FieldABC):
         attribute: str = None,
         validate: typing.Union[
             typing.Callable[[typing.Any], typing.Any],
-            typing.Iterable[typing.Callable[[typing.Any], typing.Any]],
+            typing.Sequence[typing.Callable[[typing.Any], typing.Any]],
+            typing.Generator[typing.Callable[[typing.Any], typing.Any], None, None],
         ] = None,
         required: bool = False,
         allow_none: bool = None,
@@ -561,33 +562,30 @@ class Nested(Field):
             if field.startswith(nested_field)
         ]
 
-    def _serialize(self, nested_obj, attr, obj, many=False, **kwargs):
+    def _serialize(self, nested_obj, attr, obj, **kwargs):
         # Load up the schema first. This allows a RegistryError to be raised
         # if an invalid schema name was passed
         schema = self.schema
         if nested_obj is None:
             return None
-        many = schema.many or self.many or many
-        return schema.dump(nested_obj, many=self.many or many)
+        many = schema.many or self.many
+        return schema.dump(nested_obj, many=many)
 
-    def _test_collection(self, value, many=False):
-        many = self.schema.many or self.many or many
+    def _test_collection(self, value):
+        many = self.schema.many or self.many
         if many and not utils.is_collection(value):
             raise self.make_error("type", input=value, type=value.__class__.__name__)
 
-    def _load(self, value, data, partial=None, many=False):
-        many = self.schema.many or self.many or many
+    def _load(self, value, data, partial=None):
         try:
-            valid_data = self.schema.load(
-                value, unknown=self.unknown, partial=partial, many=many
-            )
+            valid_data = self.schema.load(value, unknown=self.unknown, partial=partial)
         except ValidationError as error:
             raise ValidationError(
                 error.messages, valid_data=error.valid_data
             ) from error
         return valid_data
 
-    def _deserialize(self, value, attr, data, partial=None, many=False, **kwargs):
+    def _deserialize(self, value, attr, data, partial=None, **kwargs):
         """Same as :meth:`Field._deserialize` with additional ``partial`` argument.
 
         :param bool|tuple partial: For nested schemas, the ``partial``
@@ -596,8 +594,8 @@ class Nested(Field):
         .. versionchanged:: 3.0.0
             Add ``partial`` parameter.
         """
-        self._test_collection(value, many=many)
-        return self._load(value, data, partial=partial, many=many)
+        self._test_collection(value)
+        return self._load(value, data, partial=partial)
 
 
 class Pluck(Nested):
@@ -703,17 +701,11 @@ class List(Field):
     ) -> typing.Optional[typing.List[typing.Any]]:
         if value is None:
             return None
-        # Optimize dumping a list of Nested objects by calling dump(many=True)
-        if isinstance(self.inner, Nested) and not self.inner.many:
-            return self.inner._serialize(value, attr, obj, many=True, **kwargs)
         return [self.inner._serialize(each, attr, obj, **kwargs) for each in value]
 
     def _deserialize(self, value, attr, data, **kwargs) -> typing.List[typing.Any]:
         if not utils.is_collection(value):
             raise self.make_error("invalid")
-        # Optimize loading a list of Nested objects by calling load(many=True)
-        if isinstance(self.inner, Nested) and not self.inner.many:
-            return self.inner.deserialize(value, many=True, **kwargs)
 
         result = []
         errors = {}
diff --git a/tox.ini b/tox.ini
index 3ab5bdad..9c1cf2b7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,7 @@ extras = tests
 commands = pytest {posargs}
 
 [testenv:lint]
-deps = pre-commit>=1.20,<3.0
+deps = pre-commit~=1.20
 skip_install = true
 commands = pre-commit run --all-files --show-diff-on-failure
 


Test patch:
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index b71fed96..7790335a 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -38,6 +38,15 @@ class TestDeserializingNone:
         field = fields.Tuple([fields.String()], allow_none=True)
         assert field.deserialize(None) is None
 
+    def test_list_of_nested_allow_none_deserialize_none_to_none(self):
+        field = fields.List(fields.Nested(Schema(), allow_none=True))
+        assert field.deserialize([None]) == [None]
+
+    def test_list_of_nested_non_allow_none_deserialize_none_to_validation_error(self):
+        field = fields.List(fields.Nested(Schema(), allow_none=False))
+        with pytest.raises(ValidationError):
+            field.deserialize([None])
+
 
 class TestFieldDeserialization:
     def test_float_field_deserialization(self):