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

#852 configurable error messages #1067

Merged
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
2 changes: 0 additions & 2 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ Schema

.. autoclass:: marshmallow.SchemaOpts

.. autofunction:: marshmallow.pprint

.. _api_fields:

Fields
Expand Down
14 changes: 14 additions & 0 deletions docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
}
2 changes: 1 addition & 1 deletion marshmallow/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
19 changes: 16 additions & 3 deletions marshmallow/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ class Meta:
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

Expand Down Expand Up @@ -360,6 +367,12 @@ def __init__(
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(
Expand Down Expand Up @@ -590,7 +603,7 @@ def _deserialize(
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
Expand All @@ -608,7 +621,7 @@ def _deserialize(
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):
Expand Down Expand Up @@ -662,7 +675,7 @@ def _deserialize(
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),
)
Expand Down
80 changes: 80 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,86 @@ class ErrorSchema(Schema):
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()
Expand Down