9

This is my serializers.py (I want to create a serializer for the built-in User model):

from rest_framework import serializers

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'password', 'email', )

I'm aware that Django Rest Framework has it's own field validators, because when I try to create a user using a username which already exists, it raises an error saying:

{'username': [u'This field must be unique.']}

I want to customize the error message and make it say "This username is already taken. Please try again" rather than saying "This field must be unique".

It also has a built-in regex validator, because when I create a username with an exclamation mark, it says:

{'username': [u'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.']}

I want to customize the regex validator so that it just says "Invalid username".

How do I customize all of the error messages which each field has?

Note: according to this post: Custom error messages in Django Rest Framework serializer I can do:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User

    def __init__(self, *args, **kwargs):
        super(UserSerializer, self).__init__(*args, **kwargs)

        self.fields['username'].error_messages['required'] = u'My custom required msg'

But what do I do for the 'unique' and 'regex' validators? I tried doing

self.fields['username'].error_messages['regex'] = u'My custom required msg'

and

self.fields['username'].error_messages['validators'] = u'My custom required msg'

but neither worked.

Community
  • 1
  • 1
SilentDev
  • 20,997
  • 28
  • 111
  • 214

2 Answers2

6

In order to replace unique or regex error messages you should change message member of corresponding validator object. This could be done using separate mixin class:

from django.core.validators import RegexValidator
from rest_framework.validators import UniqueValidator
from django.utils.translation import ugettext_lazy as _


class SetCustomErrorMessagesMixin:
    """
    Replaces built-in validator messages with messages, defined in Meta class. 
    This mixin should be inherited before the actual Serializer class in order to call __init__ method.

    Example of Meta class:

    >>> class Meta:
    >>>     model = User
    >>>     fields = ('url', 'username', 'email', 'groups')
    >>>     custom_error_messages_for_validators = {
    >>>         'username': {
    >>>             UniqueValidator: _('This username is already taken. Please, try again'),
    >>>             RegexValidator: _('Invalid username')
    >>>         }
    >>>     }
    """
    def __init__(self, *args, **kwargs):
        # noinspection PyArgumentList
        super(SetCustomErrorMessagesMixin, self).__init__(*args, **kwargs)
        self.replace_validators_messages()

    def replace_validators_messages(self):
        for field_name, validators_lookup in self.custom_error_messages_for_validators.items():
            # noinspection PyUnresolvedReferences
            for validator in self.fields[field_name].validators:
                if type(validator) in validators_lookup:
                    validator.message = validators_lookup[type(validator)]

    @property
    def custom_error_messages_for_validators(self):
        meta = getattr(self, 'Meta', None)
        return getattr(meta, 'custom_error_messages_for_validators', {})

Then you could just inherit this mixin and update Meta class:

class UserSerializer(SetCustomErrorMessagesMixin, serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'groups')
        custom_error_messages_for_validators = {
            'username': {
                UniqueValidator: _('This username is already taken. Please, try again'),
                RegexValidator: _('Invalid username')
            }
        }
awesoon
  • 32,469
  • 11
  • 74
  • 99
  • Hm,you mentioned "This Could be done using separate mixin class". Is there another way to do it as well? What initially tried was using my original ModelSerializer, doing "def validate_username:" and doing my own checks and raising serializer.ValidationError() if I came across an error, but for some reason that wasn't working. This was my SO post on the issue: http://stackoverflow.com/questions/30542249/djangorestframework-modelserializer-field-level-validation-is-not-working but since you mentioned that what you showed was One way of doing it, do you think it is also the most efficient? – SilentDev Jun 01 '15 at 07:11
  • 1
    Another way is replacing all validators for a field using `Meta.extra_kwargs` field. Note, that you should provide __all__ validators, because it will replace __all__ original validators. If performance is important, you should provide some additional information, at least - expected connections per second. But if you want to replace, say 10 or even 100 messages, you will not see any difference between suggested approaches. – awesoon Jun 01 '15 at 08:03
  • ah okay, got it. Thanks. By the way, two more quick questions for clarification: 1) what's reason for using "HyperlinkedModelSerializer" in this case rather than just "ModelSerializer"? and 2) is there a standard location where custom mixins go? Or could I just put the "SetCustomErrorMessagesMixin" class in my serializers.py file? – SilentDev Jun 15 '15 at 06:17
  • 1
    [`HyperlinkedModelSerializer`](http://www.django-rest-framework.org/api-guide/serializers/#hyperlinkedmodelserializer) behaves like `ModelSerializer`, but it represents hyperlinks instead of ids. Just a matter of style. – awesoon Jun 15 '15 at 06:24
  • 1
    I always place mixins into, well, `mixins` module (like `views`, `models` and so on). – awesoon Jun 15 '15 at 06:25
  • awesome, thanks... Right, so I can use ModelSerializer in this case and it won't break any code in the long run right? (I'm asking because from my understanding, the main difference is that HyperlinkedModelSerializer's include a "url field instead of a primary key field" I'd prefer working with a primary key field). – SilentDev Jun 15 '15 at 06:28
  • 1
    Yes, you can. The `SetCustomErrorMessagesMixin` does not depend on the serializer type – awesoon Jun 15 '15 at 06:32
  • hm, okay so when I copy-pasted the code you posted and ran it, I first got an error saying "_ is not defined." The solution was to add this line to the top of the file: "from django.utils.translation import gettext as _". It then gave an error saying: "name 'UniqueValidator' is not defined". I added this line: "from rest_framework.validators import UniqueValidator, RegexValidator" and it solved the issue. But now, it gives an error saying: ""ImportError: cannot import name 'RegexValidator'"". Any idea why? – SilentDev Jun 15 '15 at 06:45
  • 1
    Seems like I forgot to add required imports, sorry. I've updated the answer, please, take look on it. – awesoon Jun 15 '15 at 06:50
1

You also can do that just customizing your language translation, look here:

https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#message-files

All you have to do is to create message files, change translation in there and compile your message into .mo files.

This method is not so much unacceptable as i might seems. Look here:

https://www.django-rest-framework.org/topics/internationalization/

DRF docs say that its common practice customizing error messages (your own and default ones) via translation.

My tips for those who`d want to try path above:

  1. Make default django.po file. Most of default errors will be there.
  2. If you are using DRF, then open this file https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po , chose error that you want to customize and copy only that error into your django.po flle above.
  3. Customize it.
  4. Compile it.

(!) NOTE Make sure that your translation file named exacly as django.po, not mylovelydjango.po, not myproject.po but EXACTLY django.po, or nothing will work.

Artem Chege
  • 189
  • 2
  • 5