Repository: marshmallow-code/marshmallow

Issue:
Change default for `unknown` to `RAISE`
As a follow up to https://github.com/marshmallow-code/marshmallow/issues/524#issuecomment-397165731 and #838 : the default for `unknown` should be changed to `RAISE`.



Discussion:

Patch:
diff --git a/marshmallow/marshalling.py b/marshmallow/marshalling.py
index a3dff0bb..88e4d3b6 100644
--- a/marshmallow/marshalling.py
+++ b/marshmallow/marshalling.py
@@ -204,7 +204,7 @@ class Unmarshaller(ErrorStore):
 
     def deserialize(
         self, data, fields_dict, many=False, partial=False,
-        unknown=EXCLUDE, dict_class=dict, index_errors=True, index=None,
+        unknown=RAISE, dict_class=dict, index_errors=True, index=None,
     ):
         """Deserialize ``data`` based on the schema defined by ``fields_dict``.
 
diff --git a/marshmallow/schema.py b/marshmallow/schema.py
index 45bca366..46f2ea1a 100644
--- a/marshmallow/schema.py
+++ b/marshmallow/schema.py
@@ -30,7 +30,7 @@ from marshmallow.decorators import (
     VALIDATES,
     VALIDATES_SCHEMA,
 )
-from marshmallow.utils import EXCLUDE, missing
+from marshmallow.utils import RAISE, missing
 
 
 def _get_fields(attrs, field_class, pop=False, ordered=False):
@@ -215,7 +215,7 @@ class SchemaOpts(object):
         self.include = getattr(meta, 'include', {})
         self.load_only = getattr(meta, 'load_only', ())
         self.dump_only = getattr(meta, 'dump_only', ())
-        self.unknown = getattr(meta, 'unknown', EXCLUDE)
+        self.unknown = getattr(meta, 'unknown', RAISE)
 
 
 class BaseSchema(base.SchemaABC):


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 9567e94a..474126e8 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -135,7 +135,7 @@ class TestPassOriginal:
                 ret['_post_dump'] = obj['sentinel']
                 return ret
 
-        schema = MySchema()
+        schema = MySchema(unknown=EXCLUDE)
         datum = {'foo': 42, 'sentinel': 24}
         item_loaded = schema.load(datum)
         assert item_loaded['foo'] == 42
@@ -174,7 +174,7 @@ class TestPassOriginal:
                     ret['_post_dump'] = original['sentinel']
                 return ret
 
-        schema = MySchema()
+        schema = MySchema(unknown=EXCLUDE)
         data = [{'foo': 42, 'sentinel': 24}, {'foo': 424, 'sentinel': 242}]
         items_loaded = schema.load(data, many=True)
         assert items_loaded == [
@@ -520,7 +520,7 @@ class TestValidatesSchemaDecorator:
                 else:
                     check(original_data)
 
-        schema = MySchema()
+        schema = MySchema(unknown=EXCLUDE)
         errors = schema.validate({'foo': 4, 'baz': 42})
         assert '_schema' in errors
         assert len(errors['_schema']) == 1
@@ -531,7 +531,7 @@ class TestValidatesSchemaDecorator:
         assert len(errors['_schema']) == 1
         assert errors['_schema'][0] == 'foo cannot be a string'
 
-        schema = MySchema()
+        schema = MySchema(unknown=EXCLUDE)
         errors = schema.validate([{'foo': 4, 'baz': 42}], many=True)
         assert '_schema' in errors
         assert len(errors['_schema']) == 1
@@ -541,20 +541,13 @@ class TestValidatesSchemaDecorator:
     def test_allow_arbitrary_field_names_in_error(self):
 
         class MySchema(Schema):
-            foo = fields.Int()
-            bar = fields.Int()
 
-            @validates_schema(pass_original=True)
-            def strict_fields(self, data, original_data):
-                for key in original_data:
-                    if key not in self.fields:
-                        raise ValidationError('Unknown field name', key)
+            @validates_schema
+            def validator(self, data):
+                raise ValidationError('Error message', 'arbitrary_key')
 
-        schema = MySchema()
-        errors = schema.validate({'foo': 2, 'baz': 42})
-        assert 'baz' in errors
-        assert len(errors['baz']) == 1
-        assert errors['baz'][0] == 'Unknown field name'
+        errors = MySchema().validate({})
+        assert errors['arbitrary_key'] == ['Error message']
 
     def test_skip_on_field_errors(self):
 
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index 640a2d2b..d842fc5d 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -803,7 +803,7 @@ class TestFieldDeserialization:
             foo = fields.Constant(42)
 
         sch = MySchema()
-        assert sch.load({'bar': 24})['foo'] == 42
+        assert sch.load({})['foo'] == 42
         assert sch.load({'foo': 24})['foo'] == 42
 
     def test_field_deserialization_with_user_validator_function(self):
@@ -934,7 +934,7 @@ class TestSchemaDeserialization:
         assert user['age'] == int(users_data[0]['age'])
 
     def test_exclude(self):
-        schema = SimpleUserSchema(exclude=('age', ))
+        schema = SimpleUserSchema(exclude=('age', ), unknown=EXCLUDE)
         result = schema.load({'name': 'Monty', 'age': 42})
         assert 'name' in result
         assert 'age' not in result
@@ -1121,7 +1121,7 @@ class TestSchemaDeserialization:
             'UserName': 'foo@bar.com',
             'years': '42',
         }
-        result = AliasingUserSerializer().load(data)
+        result = AliasingUserSerializer(unknown=EXCLUDE).load(data)
         assert result['name'] == 'Mick'
         assert result['email'] == 'foo@bar.com'
         assert 'years' not in result
@@ -1136,7 +1136,7 @@ class TestSchemaDeserialization:
             'years': '42',
             'nicknames': ['Your Majesty', 'Brenda'],
         }
-        result = AliasingUserSerializer().load(data)
+        result = AliasingUserSerializer(unknown=EXCLUDE).load(data)
         assert result['name'] == 'Mick'
         assert 'years' not in result
         assert 'nicknames' not in result
@@ -1354,7 +1354,7 @@ class TestSchemaDeserialization:
         class MySchema(Schema):
             foo = fields.Integer()
 
-        data = MySchema().load({'foo': 3, 'bar': 5})
+        data = MySchema(unknown=EXCLUDE).load({'foo': 3, 'bar': 5})
         assert data['foo'] == 3
         assert 'bar' not in data
 
@@ -1362,7 +1362,7 @@ class TestSchemaDeserialization:
         assert data['foo'] == 3
         assert 'bar' not in data
 
-        data = MySchema().load({'foo': 3, 'bar': 5}, unknown=INCLUDE)
+        data = MySchema(unknown=EXCLUDE).load({'foo': 3, 'bar': 5}, unknown=INCLUDE)
         assert data['foo'] == 3
         assert data['bar']
 
@@ -1382,13 +1382,13 @@ class TestSchemaDeserialization:
         assert 'bar' in data[1]
 
         with pytest.raises(ValidationError) as excinfo:
-            MySchema(unknown=RAISE).load({'foo': 3, 'bar': 5})
+            MySchema().load({'foo': 3, 'bar': 5})
         err = excinfo.value
         assert 'bar' in err.messages
         assert err.messages['bar'] == ['Unknown field.']
 
         with pytest.raises(ValidationError) as excinfo:
-            MySchema(unknown=RAISE, many=True).load([
+            MySchema(many=True).load([
                 {'foo': 'abc'},
                 {'foo': 3, 'bar': 5},
             ])
diff --git a/tests/test_fields.py b/tests/test_fields.py
index a1c39669..d56648ed 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import pytest
 
-from marshmallow import fields, Schema, ValidationError
+from marshmallow import fields, Schema, ValidationError, EXCLUDE
 from marshmallow.marshalling import missing
 
 from tests.base import ALL_FIELDS, User
@@ -53,7 +53,7 @@ class TestField:
         class MySchema(Schema):
             name = MyField()
 
-        result = MySchema().load({'name': 'Monty', 'foo': 42})
+        result = MySchema(unknown=EXCLUDE).load({'name': 'Monty', 'foo': 42})
         assert result == {'name': 'Monty'}
 
     def test_custom_field_receives_data_key_if_set(self, user):
@@ -66,7 +66,7 @@ class TestField:
         class MySchema(Schema):
             Name = MyField(data_key='name')
 
-        result = MySchema().load({'name': 'Monty', 'foo': 42})
+        result = MySchema(unknown=EXCLUDE).load({'name': 'Monty', 'foo': 42})
         assert result == {'Name': 'Monty'}
 
     def test_custom_field_follows_data_key_if_set(self, user):
diff --git a/tests/test_marshalling.py b/tests/test_marshalling.py
index cecf117c..440e792e 100644
--- a/tests/test_marshalling.py
+++ b/tests/test_marshalling.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from marshmallow import fields
+from marshmallow import fields, EXCLUDE, INCLUDE, RAISE
 from marshmallow.marshalling import Marshaller, Unmarshaller, missing
 from marshmallow.exceptions import ValidationError
 
@@ -125,19 +125,22 @@ class TestUnmarshaller:
     def unmarshal(self):
         return Unmarshaller()
 
-    def test_extra_data_is_ignored(self, unmarshal):
+    def test_extra_data_unknown_raise(self, unmarshal):
         fields_ = {'name': fields.Str()}
-        ret = unmarshal({'extra': 42, 'name': 'Steve'}, fields_)
+        with pytest.raises(ValidationError) as excinfo:
+            unmarshal({'extra': 42, 'name': 'Steve'}, fields_, unknown=RAISE)
+        errors = excinfo.value.messages
+        assert errors == {'extra': ['Unknown field.']}
+
+    def test_extra_data_unknown_exclude(self, unmarshal):
+        fields_ = {'name': fields.Str()}
+        ret = unmarshal({'extra': 42, 'name': 'Steve'}, fields_, unknown=EXCLUDE)
         assert 'extra' not in ret
 
-    # def test_strict_mode_many(self, unmarshal):
-    #     users = [
-    #         {'email': 'foobar'},
-    #         {'email': 'bar@example.com'}
-    #     ]
-    #     with pytest.raises(ValidationError) as excinfo:
-    #         unmarshal(users, {'email': fields.Email()}, strict=True, many=True)
-    #     assert 'Not a valid email address.' in str(excinfo)
+    def test_extra_data_unknown_include(self, unmarshal):
+        fields_ = {'name': fields.Str()}
+        ret = unmarshal({'extra': 42, 'name': 'Steve'}, fields_, unknown=INCLUDE)
+        assert ret['extra'] == 42
 
     def test_stores_errors(self, unmarshal):
         data = {'email': 'invalid-email'}
@@ -257,7 +260,7 @@ class TestUnmarshaller:
             'username': fields.Email(attribute='email', data_key='UserName'),
             'years': fields.Integer(data_key='Years'),
         }
-        result = unmarshal.deserialize(data, fields_dict)
+        result = unmarshal.deserialize(data, fields_dict, unknown=EXCLUDE)
         assert result['name'] == 'Mick'
         assert result['email'] == 'foo@bar.com'
         assert 'years' not in result
@@ -272,7 +275,7 @@ class TestUnmarshaller:
             'years': fields.Integer(dump_only=True),
             'always_invalid': fields.Field(validate=lambda f: False, dump_only=True),
         }
-        result = unmarshal.deserialize(data, fields_dict)
+        result = unmarshal.deserialize(data, fields_dict, unknown=EXCLUDE)
         assert result['name'] == 'Mick'
         assert 'years' not in result
 
diff --git a/tests/test_options.py b/tests/test_options.py
index 39684443..409e4ae7 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -4,7 +4,7 @@ import datetime as dt
 
 import pytest
 
-from marshmallow import fields, Schema
+from marshmallow import fields, Schema, EXCLUDE
 
 from tests.base import User
 
@@ -107,7 +107,7 @@ class TestFieldOrdering:
         assert keys == ['name', 'email', 'age', 'created', 'id', 'homepage', 'birthdate']
 
     def test_declared_field_order_is_maintained_on_load(self, serialized_user):
-        schema = KeepOrder()
+        schema = KeepOrder(unknown=EXCLUDE)
         data = schema.load(serialized_user)
         keys = list(data)
         assert keys == ['name', 'email', 'age', 'created', 'id', 'homepage', 'birthdate']
@@ -154,7 +154,7 @@ class TestFieldOrdering:
         assert keys == ['name', 'email', 'age', 'created', 'id', 'homepage', 'birthdate']
 
     def test_meta_fields_order_is_maintained_on_load(self, serialized_user):
-        schema = OrderedMetaSchema()
+        schema = OrderedMetaSchema(unknown=EXCLUDE)
         data = schema.load(serialized_user)
         keys = list(data)
         assert keys == ['name', 'email', 'age', 'created', 'id', 'homepage', 'birthdate']
diff --git a/tests/test_py3/test_utils.py b/tests/test_py3/test_utils.py
index 07bfac2d..17b601b5 100644
--- a/tests/test_py3/test_utils.py
+++ b/tests/test_py3/test_utils.py
@@ -9,6 +9,6 @@ def test_function_field_using_type_annotation():
     class MySchema(Schema):
         friends = fields.Function(deserialize=get_split_words)
 
-    data = {'name': 'Bruce Wayne', 'friends': 'Clark;Alfred;Robin'}
+    data = {'friends': 'Clark;Alfred;Robin'}
     result = MySchema().load(data)
     assert result == {'friends': ['Clark', 'Alfred', 'Robin']}
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 678f1b5b..c77b0f72 100755
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -9,7 +9,7 @@ from collections import namedtuple, OrderedDict
 
 import pytest
 
-from marshmallow import Schema, fields, utils, validates, validates_schema
+from marshmallow import Schema, fields, utils, validates, validates_schema, EXCLUDE
 from marshmallow.exceptions import ValidationError
 
 from tests.base import (
@@ -177,6 +177,7 @@ def test_dump_resets_error_fields():
 def test_load_resets_error_fields():
     class MySchema(Schema):
         email = fields.Email()
+        name = fields.Str()
 
     schema = MySchema()
     with pytest.raises(ValidationError) as excinfo:
@@ -185,8 +186,8 @@ def test_load_resets_error_fields():
     assert len(exc.field_names) == 1
 
     with pytest.raises(ValidationError) as excinfo:
-        schema.load({'name': 'Joe', 'email': '__invalid'})
-
+        schema.load({'name': 12, 'email': 'mick@stones.com'})
+    exc = excinfo.value
     assert len(exc.field_names) == 1
 
 def test_load_resets_error_kwargs():
@@ -1176,7 +1177,7 @@ class TestDeeplyNestedLoadOnly:
         assert 'str_regular' in grand_child
 
     def test_dump_only(self, schema, data):
-        result = schema.load(data)
+        result = schema.load(data, unknown=EXCLUDE)
         assert 'str_dump_only' not in result
         assert 'str_load_only' in result
         assert 'str_regular' in result
@@ -1234,7 +1235,7 @@ class TestDeeplyNestedListLoadOnly:
         assert 'str_regular' in child
 
     def test_dump_only(self, schema, data):
-        result = schema.load(data)
+        result = schema.load(data, unknown=EXCLUDE)
         assert 'str_dump_only' not in result
         assert 'str_load_only' in result
         assert 'str_regular' in result
@@ -2521,7 +2522,7 @@ class TestLoadOnly:
         assert 'str_regular' in result
 
     def test_dump_only(self, schema, data):
-        result = schema.load(data)
+        result = schema.load(data, unknown=EXCLUDE)
         assert 'str_dump_only' not in result
         assert 'str_load_only' in result
         assert 'str_regular' in result