Repository: marshmallow-code/marshmallow

Issue:
only is not bound by declared and additional fields on serializaton
The only parameter is correctly bound to the fields that are defined in Meta.fields (#183):
```python
class MySchema(Schema):
    class Meta:
        fields = ('a', )

MySchema(only=('b', )).dump({'a': 1, 'b': 2}).data == {}
```


Strangely it is not bound to the declared and additional fields:
```python
class MySchema(Schema):
    a = fields.Str()
    class Meta:
        additional = ('b', )

MySchema(only=('c', )).dump({'a': 1, 'b': 2, 'c': 3}).data == {'c': 3}
```

It would be more consistent if the second example also returns an empty dict.



Discussion:

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index e09e780c..3d135fbf 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -102,4 +102,5 @@ Contributors (chronological)
 - Suren Khorenyan `@surik00 <https://github.com/surik00>`_
 - Jeffrey Berger `@JeffBerger <https://github.com/JeffBerger>`_
 - Felix Yan `@felixonmars <https://github.com/felixonmars>`_
+- Prasanjit Prakash `@ikilledthecat <https://github.com/ikilledthecat>`_
 - Guillaume Gelin `@ramnes <https://github.com/ramnes>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index cbcdedc9..48ac339a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,10 @@ Other changes:
 - *Backwards-incompatible*: Pre/Post-processors MUST return modified data.
   Returning ``None`` does not imply data were mutated (:issue:`347`). Thanks
   :user:`tdevelioglu` for reporting.
+- *Backwards-incompatible*: ``only`` and ``exclude`` are bound by
+  declared and additional fields. A ``ValueError`` is raised if invalid
+  fields are passed (:issue:`636`). Thanks :user:`jan-23` for reporting.
+  Thanks :user:`ikilledthecat` and :user:`deckar01` for the PRs.
 
 Deprecations/Removals:
 
diff --git a/marshmallow/schema.py b/marshmallow/schema.py
index 9c36887f..45bca366 100644
--- a/marshmallow/schema.py
+++ b/marshmallow/schema.py
@@ -247,12 +247,13 @@ class BaseSchema(base.SchemaABC):
         data, errors = schema.dump(album)
         data  # {'release_date': '1968-12-06', 'title': 'Beggars Banquet'}
 
-    :param tuple|list only: Whitelist of fields to select when instantiating the Schema.
-        If None, all fields are used.
-        Nested fields can be represented with dot delimiters.
-    :param tuple|list exclude: Blacklist of fields to exclude when instantiating the Schema.
-        If a field appears in both `only` and `exclude`, it is not used.
-        Nested fields can be represented with dot delimiters.
+    :param tuple|list only: Whitelist of the declared fields to select when
+        instantiating the Schema. If None, all fields are used. Nested fields
+        can be represented with dot delimiters.
+    :param tuple|list exclude: Blacklist of the declared fields to exclude
+        when instantiating the Schema. If a field appears in both `only` and
+        `exclude`, it is not used. Nested fields can be represented with dot
+        delimiters.
     :param str prefix: Optional prefix that will be prepended to all the
         serialized field names.
     :param bool many: Should be set to `True` if ``obj`` is a collection
@@ -723,13 +724,7 @@ class BaseSchema(base.SchemaABC):
 
     def _update_fields(self, obj=None, many=False):
         """Update fields based on the passed in object."""
-        if self.only is not None:
-            # Return only fields specified in only option
-            if self.opts.fields:
-                field_names = self.set_class(self.opts.fields) & self.set_class(self.only)
-            else:
-                field_names = self.set_class(self.only)
-        elif self.opts.fields:
+        if self.opts.fields:
             # Return fields specified in fields option
             field_names = self.set_class(self.opts.fields)
         elif self.opts.additional:
@@ -739,10 +734,26 @@ class BaseSchema(base.SchemaABC):
         else:
             field_names = self.set_class(self.declared_fields.keys())
 
+        declared_fields = field_names
+        invalid_fields = self.set_class()
+        if self.only is not None:
+            # Return only fields specified in only option
+            only = self.set_class(self.only)
+            field_names = only
+            invalid_only = only - declared_fields
+            invalid_fields |= invalid_only
+
         # If "exclude" option or param is specified, remove those fields
         excludes = set(self.opts.exclude) | set(self.exclude)
         if excludes:
             field_names = field_names - excludes
+            invalid_excludes = excludes - declared_fields
+            invalid_fields |= invalid_excludes
+
+        if invalid_fields:
+            message = 'Invalid fields for {0}: {1}.'.format(self, invalid_fields)
+            raise ValueError(message)
+
         ret = self.__filter_fields(field_names, obj, many=many)
         # Set parents
         self.__set_field_attrs(ret)


Test patch:
diff --git a/tests/base.py b/tests/base.py
index d335ed45..a9e79163 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -229,7 +229,7 @@ class UserMetaSchema(Schema):
 
 class UserExcludeSchema(UserSchema):
     class Meta:
-        exclude = ('created', 'updated', 'field_not_found_but_thats_ok')
+        exclude = ('created', 'updated',)
 
 
 class UserAdditionalSchema(Schema):
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 65fc487c..678f1b5b 100755
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -671,7 +671,7 @@ def test_only_in_init(SchemaClass, user):
     assert 'age' in s
 
 def test_invalid_only_param(user):
-    with pytest.raises(AttributeError):
+    with pytest.raises(ValueError):
         UserSchema(only=('_invalid', 'name')).dump(user)
 
 def test_can_serialize_uuid(serialized_user, user):
@@ -1291,23 +1291,70 @@ def test_only_and_exclude():
     assert 'bar' not in result
 
 
-def test_exclude_invalid_attribute():
+def test_only_and_exclude_with_fields():
+    class MySchema(Schema):
+        foo = fields.Field()
+
+        class Meta:
+            fields = ('bar', 'baz')
+    sch = MySchema(only=('bar', 'baz'), exclude=('bar', ))
+    data = dict(foo=42, bar=24, baz=242)
+    result = sch.dump(data)
+    assert 'baz' in result
+    assert 'bar' not in result
+
 
+def test_invalid_only_and_exclude_with_fields():
     class MySchema(Schema):
         foo = fields.Field()
 
-    sch = MySchema(exclude=('bar', ))
-    assert sch.dump({'foo': 42}) == {'foo': 42}
+        class Meta:
+            fields = ('bar', 'baz')
 
+    with pytest.raises(ValueError) as excinfo:
+        MySchema(only=('foo', 'par'), exclude=('ban', ))
 
-def test_only_with_invalid_attribute():
+    assert 'foo' in str(excinfo)
+    assert 'par' in str(excinfo)
+    assert 'ban' in str(excinfo)
+
+
+def test_only_and_exclude_with_additional():
     class MySchema(Schema):
         foo = fields.Field()
 
-    sch = MySchema(only=('bar', ))
-    with pytest.raises(KeyError) as excinfo:
-        sch.dump(dict(foo=42))
-    assert '"bar" is not a valid field' in str(excinfo.value.args[0])
+        class Meta:
+            additional = ('bar', 'baz')
+    sch = MySchema(only=('foo', 'bar'), exclude=('bar', ))
+    data = dict(foo=42, bar=24, baz=242)
+    result = sch.dump(data)
+    assert 'foo' in result
+    assert 'bar' not in result
+
+
+def test_invalid_only_and_exclude_with_additional():
+    class MySchema(Schema):
+        foo = fields.Field()
+
+        class Meta:
+            additional = ('bar', 'baz')
+
+    with pytest.raises(ValueError) as excinfo:
+        MySchema(only=('foop', 'par'), exclude=('ban', ))
+
+    assert 'foop' in str(excinfo)
+    assert 'par' in str(excinfo)
+    assert 'ban' in str(excinfo)
+
+
+def test_exclude_invalid_attribute():
+
+    class MySchema(Schema):
+        foo = fields.Field()
+
+    with pytest.raises(ValueError):
+        MySchema(exclude=('bar', ))
+
 
 def test_only_bounded_by_fields():
     class MySchema(Schema):
@@ -1315,8 +1362,18 @@ def test_only_bounded_by_fields():
         class Meta:
             fields = ('foo', )
 
-    sch = MySchema(only=('baz', ))
-    assert sch.dump({'foo': 42}) == {}
+    with pytest.raises(ValueError):
+        MySchema(only=('baz', ))
+
+
+def test_only_bounded_by_additional():
+    class MySchema(Schema):
+
+        class Meta:
+            additional = ('b', )
+
+    with pytest.raises(ValueError):
+        MySchema(only=('c', )).dump({'c': 3})
 
 def test_only_empty():
     class MySchema(Schema):