Skip to content

Commit e1acdd7

Browse files
committed
Fix load parameter *unknow* propagation
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 #1428
1 parent a74f38d commit e1acdd7

File tree

5 files changed

+84
-6
lines changed

5 files changed

+84
-6
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,4 @@ Contributors (chronological)
140140
- `@phrfpeixoto <https://github.com/phrfpeixoto>`_
141141
- `@jceresini <https://github.com/jceresini>`_
142142
- Nikolay Shebanov `@killthekitten <https://github.com/killthekitten>`_
143+
- Laurent Mignon `@lmignon <https://github.com/lmignon>`_

docs/quickstart.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,55 @@ or when calling `load <Schema.load>`.
427427
428428
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*.
429429

430+
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*.
431+
432+
.. code-block:: python
433+
434+
from marshmallow import fields, Schema, EXCLUDE, INCLUDE, RAISE
435+
436+
437+
class UserSchema(Schema):
438+
class Meta:
439+
unknown = RAISE
440+
441+
name = fields.String()
442+
443+
444+
class BlogSchema(Schema):
445+
class Meta:
446+
unknown = EXCLUDE
447+
448+
title = fields.String()
449+
author = fields.Nested(UserSchema)
450+
451+
452+
BlogSchema().load(
453+
{
454+
"title": "Some title",
455+
"unknown_field": "value",
456+
"author": {"name": "Author name", "unknown_field": "value"},
457+
}
458+
) # => ValidationError: {'author': {'unknown_field': ['Unknown field.']}}
459+
460+
BlogSchema().load(
461+
{
462+
"title": "Some title",
463+
"unknown_field": "value",
464+
"author": {"name": "Author name", "unknown_field": "value"},
465+
},
466+
unknown=EXCLUDE,
467+
) # => {'author': {'name': 'Author name'}, 'title': 'Some title'}
468+
469+
470+
BogSchema.load(
471+
{
472+
"title": "Some title",
473+
"unknown_field": "value",
474+
"author": {"name": "Author name", "unknown_field": "value"},
475+
},
476+
unknown=INCLUDE,
477+
) # => {'author': {'name': 'Author name', 'unknown_field': 'value'}, 'title': 'Some title', 'unknown_field': 'value'}
478+
430479
This order of precedence allows you to change the behavior of a schema for different contexts.
431480

432481

src/marshmallow/fields.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,29 +567,36 @@ def _test_collection(self, value, many=False):
567567
if many and not utils.is_collection(value):
568568
raise self.make_error("type", input=value, type=value.__class__.__name__)
569569

570-
def _load(self, value, data, partial=None, many=False):
570+
def _load(self, value, data, partial=None, many=False, unknown=None):
571571
many = self.schema.many or self.many or many
572+
unknown = unknown or self.unknown
572573
try:
573574
valid_data = self.schema.load(
574-
value, unknown=self.unknown, partial=partial, many=many
575+
value, unknown=unknown, partial=partial, many=many
575576
)
576577
except ValidationError as error:
577578
raise ValidationError(
578579
error.messages, valid_data=error.valid_data
579580
) from error
580581
return valid_data
581582

582-
def _deserialize(self, value, attr, data, partial=None, many=False, **kwargs):
583+
def _deserialize(
584+
self, value, attr, data, partial=None, many=False, unknown=None, **kwargs
585+
):
583586
"""Same as :meth:`Field._deserialize` with additional ``partial`` argument.
584587
585588
:param bool|tuple partial: For nested schemas, the ``partial``
586589
parameter passed to `Schema.load`.
590+
:param unknown: For nested schemas, the ``unknown``
591+
parameter passed to `Schema.load`..
587592
588593
.. versionchanged:: 3.0.0
589594
Add ``partial`` parameter.
595+
.. versionchanged:: 3.2.2
596+
Add ``unknown`` parameter.
590597
"""
591598
self._test_collection(value, many=many)
592-
return self._load(value, data, partial=partial, many=many)
599+
return self._load(value, data, partial=partial, many=many, unknown=unknown)
593600

594601

595602
class Pluck(Nested):

src/marshmallow/schema.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ def _deserialize(
652652
d_kwargs["partial"] = sub_partial
653653
else:
654654
d_kwargs["partial"] = partial
655+
d_kwargs["unknown"] = unknown
655656
getter = lambda val: field_obj.deserialize(
656657
val, field_name, data, **d_kwargs
657658
)
@@ -665,6 +666,7 @@ def _deserialize(
665666
if value is not missing:
666667
key = field_obj.attribute or attr_name
667668
set_value(typing.cast(typing.Dict, ret), key, value)
669+
unknown = unknown or self.unknown
668670
if unknown != EXCLUDE:
669671
fields = {
670672
field_obj.data_key if field_obj.data_key is not None else field_name
@@ -701,14 +703,17 @@ def load(
701703
will be ignored. Use dot delimiters to specify nested fields.
702704
:param unknown: Whether to exclude, include, or raise an error for unknown
703705
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
704-
If `None`, the value for `self.unknown` is used.
706+
If `None`, the value for `self.unknown` is used. When used, provided
707+
value is propagated to the loading of nested schemas.
705708
:return: Deserialized data
706709
707710
.. versionadded:: 1.0.0
708711
.. versionchanged:: 3.0.0b7
709712
This method returns the deserialized data rather than a ``(data, errors)`` duple.
710713
A :exc:`ValidationError <marshmallow.exceptions.ValidationError>` is raised
711714
if invalid data are passed.
715+
.. versionchanged:: 3.2.2
716+
The ``unknown`` parameter value is propagated to the loading of nested schemas.
712717
"""
713718
return self._do_load(
714719
data, many=many, partial=partial, unknown=unknown, postprocess=True
@@ -823,7 +828,6 @@ def _do_load(
823828
error_store = ErrorStore()
824829
errors = {} # type: typing.Dict[str, typing.List[str]]
825830
many = self.many if many is None else bool(many)
826-
unknown = unknown or self.unknown
827831
if partial is None:
828832
partial = self.partial
829833
# Run preprocessors

tests/test_schema.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ class Outer(Schema):
289289
assert Outer().load({"list1": val, "list2": val}) == {"list1": [], "list2": []}
290290

291291

292+
@pytest.mark.parametrize(
293+
"val",
294+
(
295+
{"inner": {"name": "name"}, "unknown": 1},
296+
{"inner": {"name": "name", "unknown_nested": 1}, "unknown": 1},
297+
),
298+
)
299+
def test_load_unknown(val):
300+
class Inner(Schema):
301+
name = fields.String()
302+
303+
class Outer(Schema):
304+
inner = fields.Nested(Inner)
305+
306+
assert Outer().load(val, unknown=EXCLUDE) == {"inner": {"name": "name"}}
307+
308+
292309
def test_loads_returns_a_user():
293310
s = UserSchema()
294311
result = s.loads(json.dumps({"name": "Monty"}))

0 commit comments

Comments
 (0)