Today I found myself in need of extending the way Django does model validation. What I needed was a partial unique index. Unfortunately Django doesn't have support for that built in. What I ended up doing was overriding my model's validate_unique() method. This wasn't terribly difficult, but did have some complications due to a bug in django.core.exceptions.ValidationError or unclear documentation. The examples in the documentation only show ValidationError being raised with a string passed into it.

The problem that with this is that part of the code for ValidationError assumes that you did not pass in a string but instead passed in a dict. In fact the comments in __init()__ actually say that a string is what is usually passed in.

def __init__(self, message, code=None, params=None):
    import operator
    from django.utils.encoding import force_unicode
    ValidationError can be passed any object that can be printed (usually a string), a list of objects or a dictionary.
    if isinstance(message, dict):
        self.message_dict = message
        # Reduce each list of messages into a single list.
        message = reduce(operator.add, message.values())

    if isinstance(message, list):
        self.messages = [force_unicode(msg) for msg in message]
        self.code = code
        self.params = params
        message = force_unicode(message)
        self.messages = [message]

But even though we could pass in a dict, list, or string it blows up if you pass a string. This bug is a bit old and the line numbers are no longer correct it seems, but this error is happening in Django 1.3.1. I'll dig up relevant info from a stack trace later, if I remember.

So to resolve the issue I looked at the format of the dict that ValidationError was looking at if the dict exists. It is a dict keyed to the name of the attribute that the error is related to and the value is a list of error messages for that attribute. This is actually the behavior I wanted anyway, since just passing a string, the error can't be tied to the attribute which is having a problem. The model below will fail a call to full_clean() every time but illustrates how to raise the ValidationError so that Django behaves properly. This can be done from validate_unique() as I did since my real use case was validating uniqueness or it can be done from clean().

class MyModel(models.Model):
    my_attribute = models.CharField(max_length=2)

    def validate_unique(self, *args, **kwargs):
        from django.core.exceptions import ValidationError
        # This example will always fail to validate
        raise ValidationError({'my_attribute': ('''something has gone wrong!''',),})
        super(MyModel, self).validate_unique(*args, **kwargs)

In my case there is still a need for further checks. This validates the uniqueness, but it is not atomic with the actual save so a different process could sneak in and do a save which makes the current model no longer unique in between my model validating uniqueness and actually saving. My current solution to this is to manually add a partial unique index to the table so that the database layer will throw and exception when I call save() if this situation happens.