Skip to content

Commit

Permalink
Fix load parameter *unknow* propagation
Browse files Browse the repository at this point in the history
When deserializing a data structure with the load method, the *unknown* was not propagated to the loading of nested data structures. As result, if a unknown field was present into a nested data structure a ValidationError was raised even if the load methd was called with *unknown=EXCLUDE*. This commit ensures that this parameter is now propagated also to the loading of nested data structures.
fixes marshmallow-code#1428
  • Loading branch information
lmignon committed Nov 2, 2019
1 parent a74f38d commit e1acdd7
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 6 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,4 @@ Contributors (chronological)
- `@phrfpeixoto <https://github.com/phrfpeixoto>`_
- `@jceresini <https://github.com/jceresini>`_
- Nikolay Shebanov `@killthekitten <https://github.com/killthekitten>`_
- Laurent Mignon `@lmignon <https://github.com/lmignon>`_
49 changes: 49 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,55 @@ or when calling `load <Schema.load>`.
The ``unknown`` option value set in :meth:`load <Schema.load>` will override the value applied at instantiation time, which itself will override the value defined in the *class Meta*.

The ``unknown`` option value set in :meth:`load <Schema.load>` is propagated to nested Schema. It's not the case when the value is applied at instantiation time nor when the value is defined in the *class Meta*.

.. code-block:: python
from marshmallow import fields, Schema, EXCLUDE, INCLUDE, RAISE
class UserSchema(Schema):
class Meta:
unknown = RAISE
name = fields.String()
class BlogSchema(Schema):
class Meta:
unknown = EXCLUDE
title = fields.String()
author = fields.Nested(UserSchema)
BlogSchema().load(
{
"title": "Some title",
"unknown_field": "value",
"author": {"name": "Author name", "unknown_field": "value"},
}
) # => ValidationError: {'author': {'unknown_field': ['Unknown field.']}}
BlogSchema().load(
{
"title": "Some title",
"unknown_field": "value",
"author": {"name": "Author name", "unknown_field": "value"},
},
unknown=EXCLUDE,
) # => {'author': {'name': 'Author name'}, 'title': 'Some title'}
BogSchema.load(
{
"title": "Some title",
"unknown_field": "value",
"author": {"name": "Author name", "unknown_field": "value"},
},
unknown=INCLUDE,
) # => {'author': {'name': 'Author name', 'unknown_field': 'value'}, 'title': 'Some title', 'unknown_field': 'value'}
This order of precedence allows you to change the behavior of a schema for different contexts.


Expand Down
15 changes: 11 additions & 4 deletions src/marshmallow/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,29 +567,36 @@ def _test_collection(self, value, many=False):
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):
def _load(self, value, data, partial=None, many=False, unknown=None):
many = self.schema.many or self.many or many
unknown = unknown or self.unknown
try:
valid_data = self.schema.load(
value, unknown=self.unknown, partial=partial, many=many
value, unknown=unknown, partial=partial, many=many
)
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, many=False, unknown=None, **kwargs
):
"""Same as :meth:`Field._deserialize` with additional ``partial`` argument.
:param bool|tuple partial: For nested schemas, the ``partial``
parameter passed to `Schema.load`.
:param unknown: For nested schemas, the ``unknown``
parameter passed to `Schema.load`..
.. versionchanged:: 3.0.0
Add ``partial`` parameter.
.. versionchanged:: 3.2.2
Add ``unknown`` parameter.
"""
self._test_collection(value, many=many)
return self._load(value, data, partial=partial, many=many)
return self._load(value, data, partial=partial, many=many, unknown=unknown)


class Pluck(Nested):
Expand Down
8 changes: 6 additions & 2 deletions src/marshmallow/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ def _deserialize(
d_kwargs["partial"] = sub_partial
else:
d_kwargs["partial"] = partial
d_kwargs["unknown"] = unknown
getter = lambda val: field_obj.deserialize(
val, field_name, data, **d_kwargs
)
Expand All @@ -665,6 +666,7 @@ def _deserialize(
if value is not missing:
key = field_obj.attribute or attr_name
set_value(typing.cast(typing.Dict, ret), key, value)
unknown = unknown or self.unknown
if unknown != EXCLUDE:
fields = {
field_obj.data_key if field_obj.data_key is not None else field_name
Expand Down Expand Up @@ -701,14 +703,17 @@ def load(
will be ignored. Use dot delimiters to specify nested fields.
:param unknown: Whether to exclude, include, or raise an error for unknown
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
If `None`, the value for `self.unknown` is used.
If `None`, the value for `self.unknown` is used. When used, provided
value is propagated to the loading of nested schemas.
:return: Deserialized data
.. versionadded:: 1.0.0
.. versionchanged:: 3.0.0b7
This method returns the deserialized data rather than a ``(data, errors)`` duple.
A :exc:`ValidationError <marshmallow.exceptions.ValidationError>` is raised
if invalid data are passed.
.. versionchanged:: 3.2.2
The ``unknown`` parameter value is propagated to the loading of nested schemas.
"""
return self._do_load(
data, many=many, partial=partial, unknown=unknown, postprocess=True
Expand Down Expand Up @@ -823,7 +828,6 @@ def _do_load(
error_store = ErrorStore()
errors = {} # type: typing.Dict[str, typing.List[str]]
many = self.many if many is None else bool(many)
unknown = unknown or self.unknown
if partial is None:
partial = self.partial
# Run preprocessors
Expand Down
17 changes: 17 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,23 @@ class Outer(Schema):
assert Outer().load({"list1": val, "list2": val}) == {"list1": [], "list2": []}


@pytest.mark.parametrize(
"val",
(
{"inner": {"name": "name"}, "unknown": 1},
{"inner": {"name": "name", "unknown_nested": 1}, "unknown": 1},
),
)
def test_load_unknown(val):
class Inner(Schema):
name = fields.String()

class Outer(Schema):
inner = fields.Nested(Inner)

assert Outer().load(val, unknown=EXCLUDE) == {"inner": {"name": "name"}}


def test_loads_returns_a_user():
s = UserSchema()
result = s.loads(json.dumps({"name": "Monty"}))
Expand Down

0 comments on commit e1acdd7

Please sign in to comment.