Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix load parameter *unknow* propagation #1429

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
23 changes: 23 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,29 @@ 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):
lmignon marked this conversation as resolved.
Show resolved Hide resolved
name = fields.String()

class Meta:
unknown = RAISE

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

class Meta:
unknown = RAISE

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