Repository: marshmallow-code/marshmallow

Issue:
Make the error messages for "unknown fields" and "invalid data type" configurable
As a follow-up to https://github.com/marshmallow-code/marshmallow/pull/838#issuecomment-399677937 : Make the error messages for passing unknown fields (currently `'Unknown field.'`) and invalid input type (currently `'Invalid input type.'`) configurable on a `Schema`.

Discussion:

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index f1f23ff6..f3933100 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -115,4 +115,5 @@ Contributors (chronological)
 - Jan Margeta `@jmargeta <https://github.com/jmargeta>`_
 - AlexV `@asmodehn <https://github.com/asmodehn>`_
 - `@toffan <https://github.com/toffan>`_
+- Hampus Dunström `@Dunstrom <https://github.com/Dunstrom>`_
 - Robert Jensen `@r1b <https://github.com/r1b>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1770ddac..56eb330d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,8 @@ Changelog
 
 Features:
 
+- Make the error messages for "unknown fields" and "invalid data type"
+  configurable (:issue:`852`). Thanks :user:`Dunstrom` for the PR.
 - ``fields.Boolean`` parses ``"yes"``/``"no"`` values (:pr:`1081`).
   Thanks :user:`r1b`.
 
diff --git a/docs/api_reference.rst b/docs/api_reference.rst
index 453a7801..b4bd3146 100644
--- a/docs/api_reference.rst
+++ b/docs/api_reference.rst
@@ -15,8 +15,6 @@ Schema
 
 .. autoclass:: marshmallow.SchemaOpts
 
-.. autofunction:: marshmallow.pprint
-
 .. _api_fields:
 
 Fields
diff --git a/docs/extending.rst b/docs/extending.rst
index 072d7b0f..4721c56e 100644
--- a/docs/extending.rst
+++ b/docs/extending.rst
@@ -462,3 +462,17 @@ The ``context`` attribute of a `Schema` is a general-purpose store for extra inf
     # custom fields, schema methods, schema validators, etc.
     schema.context['request'] = request
     schema.dump(user)
+
+Custom Error Messages
+---------------------
+
+You can customize the error messages that `dump <marshmallow.Schema.dump>` and `dumps <marshmallow.Schema.dumps>` uses when raising a `ValidationError <marshmallow.exceptions.ValidationError>`.
+You do this by overriding the ``error_messages`` class variable:
+
+.. code-block:: python
+
+    class MySchema(Schema):
+        error_messages = {
+            'unknown': 'Custom unknown field error message.',
+            'type': 'Custom invalid type error message.'
+        }
diff --git a/marshmallow/fields.py b/marshmallow/fields.py
index db29267d..01cff48d 100644
--- a/marshmallow/fields.py
+++ b/marshmallow/fields.py
@@ -117,7 +117,7 @@ class Field(FieldABC):
 
     #: Default error messages for various kinds of errors. The keys in this dictionary
     #: are passed to `Field.fail`. The values are error messages passed to
-    #: :exc:`marshmallow.ValidationError`.
+    #: :exc:`marshmallow.exceptions.ValidationError`.
     default_error_messages = {
         'required': 'Missing data for required field.',
         'null': 'Field may not be null.',
diff --git a/marshmallow/schema.py b/marshmallow/schema.py
index 93167ba5..14db0076 100644
--- a/marshmallow/schema.py
+++ b/marshmallow/schema.py
@@ -293,6 +293,13 @@ class BaseSchema(base.SchemaABC):
         dt.timedelta: ma_fields.TimeDelta,
         decimal.Decimal: ma_fields.Decimal,
     }
+    #: Overrides for default schema-level error messages
+    error_messages = {}
+
+    _default_error_messages = {
+        'type': 'Invalid input type.',
+        'unknown': 'Unknown field.',
+    }
 
     OPTIONS_CLASS = SchemaOpts
 
@@ -360,6 +367,12 @@ class BaseSchema(base.SchemaABC):
         self._normalize_nested_options()
         #: Dictionary mapping field_names -> :class:`Field` objects
         self.fields = self._init_fields()
+        messages = {}
+        messages.update(self._default_error_messages)
+        for cls in reversed(self.__class__.__mro__):
+            messages.update(getattr(cls, 'error_messages', {}))
+        messages.update(self.error_messages or {})
+        self.error_messages = messages
 
     def __repr__(self):
         return '<{ClassName}(many={self.many})>'.format(
@@ -590,7 +603,7 @@ class BaseSchema(base.SchemaABC):
         index = index if index_errors else None
         if many:
             if not is_collection(data):
-                error_store.store_error(['Invalid input type.'], index=index)
+                error_store.store_error([self.error_messages['type']], index=index)
                 ret = []
             else:
                 self._pending = True
@@ -608,7 +621,7 @@ class BaseSchema(base.SchemaABC):
         ret = dict_class()
         # Check data is a dict
         if not isinstance(data, Mapping):
-            error_store.store_error(['Invalid input type.'], index=index)
+            error_store.store_error([self.error_messages['type']], index=index)
         else:
             partial_is_collection = is_collection(partial)
             for attr_name, field_obj in iteritems(fields_dict):
@@ -662,7 +675,7 @@ class BaseSchema(base.SchemaABC):
                         set_value(ret, key, value)
                     elif unknown == RAISE:
                         error_store.store_error(
-                            ['Unknown field.'],
+                            [self.error_messages['unknown']],
                             key,
                             (index if index_errors else None),
                         )


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index fdaa6311..83f20306 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -786,6 +786,86 @@ def test_custom_error_message():
     assert 'Invalid email' in errors['email']
 
 
+def test_custom_unknown_error_message():
+    custom_message = 'custom error message.'
+
+    class ErrorSchema(Schema):
+        error_messages = {'unknown': custom_message}
+        name = fields.String()
+
+    s = ErrorSchema()
+    u = {'name': 'Joe', 'age': 13}
+    with pytest.raises(ValidationError) as excinfo:
+        s.load(u)
+    errors = excinfo.value.messages
+    assert custom_message in errors['age']
+
+
+def test_custom_type_error_message():
+    custom_message = 'custom error message.'
+
+    class ErrorSchema(Schema):
+        error_messages = {'type': custom_message}
+        name = fields.String()
+
+    s = ErrorSchema()
+    u = ['Joe']
+    with pytest.raises(ValidationError) as excinfo:
+        s.load(u)
+    errors = excinfo.value.messages
+    assert custom_message in errors['_schema']
+
+
+def test_custom_type_error_message_with_many():
+    custom_message = 'custom error message.'
+
+    class ErrorSchema(Schema):
+        error_messages = {'type': custom_message}
+        name = fields.String()
+
+    s = ErrorSchema(many=True)
+    u = {'name': 'Joe'}
+    with pytest.raises(ValidationError) as excinfo:
+        s.load(u)
+    errors = excinfo.value.messages
+    assert custom_message in errors['_schema']
+
+
+def test_custom_error_messages_with_inheritance():
+    parent_type_message = 'parent type error message.'
+    parent_unknown_message = 'parent unknown error message.'
+    child_type_message = 'child type error message.'
+
+    class ParentSchema(Schema):
+        error_messages = {
+            'type': parent_type_message,
+            'unknown': parent_unknown_message,
+        }
+        name = fields.String()
+
+    class ChildSchema(ParentSchema):
+        error_messages = {'type': child_type_message}
+
+    unknown_user = {'name': 'Eleven', 'age': 12}
+    type_user = 11
+
+    parent_schema = ParentSchema()
+    with pytest.raises(ValidationError) as excinfo:
+        parent_schema.load(unknown_user)
+    assert parent_unknown_message in excinfo.value.messages['age']
+    with pytest.raises(ValidationError) as excinfo:
+        parent_schema.load(type_user)
+    assert parent_type_message in excinfo.value.messages['_schema']
+
+    child_schema = ChildSchema()
+    with pytest.raises(ValidationError) as excinfo:
+        child_schema.load(unknown_user)
+    assert parent_unknown_message in excinfo.value.messages['age']
+    with pytest.raises(ValidationError) as excinfo:
+        child_schema.load(type_user)
+    assert child_type_message in excinfo.value.messages['_schema']
+
+
 def test_load_errors_with_many():
     class ErrorSchema(Schema):
         email = fields.Email()