Custom Error Handling for Duplicate Key Errors in Django Admin

Hello fellow Django enthusiasts,

I’ve been trying to achieve a specific customization in the Django Admin regarding duplicate key errors, and I’m looking for some guidance or insights on how to best approach this.

I’m working on a project where I want to handle duplicate key errors in the Django Admin in a more user-friendly way. Specifically, I want to display a custom validation error message to the user when they attempt to save a record that would result in a duplicate key violation.

I’ve tried a few approaches, including overriding the save_model method and raising a ValidationError with my custom error message. However, in all attempts, I’ve encountered challenges like the default success message being displayed alongside the custom error message, the ValidationError being raised to the user instead of being shown a validation error message, or the error not being handled at all.

I’m aiming to achieve the following:

  1. When a duplicate key error occurs, display my custom error message to the user (not the error page).
  2. Prevent the default success message from being shown.

The latest approach I’ve tried involves overriding the save_model method to handle in the Django Admin.

Code (simplified):

    def save_model(self, request, obj, form, change):
        assert request.user.account is not None
        obj.owner = request.user.account

        try:
            super().save_model(request, obj, form, change)
        except IntegrityError as err:
            if "identifier" in str(err):
                raise ValidationError({"identifier": "This identifier is already in use."}) from err
            raise err

While I’m able to catch the duplicate key error and raise a ValidationError , the issue is that this error is being directly displayed to the user. And (in debug mode) I see the yellow error page with the following error message:

ValidationError at /admin/app/my_model/add/
{'identifier': ['This identifier is already in use.']}

I’d greatly appreciate any suggestions, advice, or alternative approaches you might have to achieve my goal. If anyone has successfully tackled a similar scenario or can shed light on how to suppress the default success message while showing only a custom error message for duplicate key errors, I’d be very grateful to learn from your experience.

Thank you in advance for your assistance and insights!

Best regards.

What I think you could do would be to define a custom form for this model, with a custom clean method. This clean method would check to see if a duplicate value already exists in the database, and raises the validation error at that point.

You want to catch this before it tries to save the form.

I also tried this, but maybe I am doing something wrong:

This is my code:

class MyAdminForm(forms.ModelForm):

    def clean(self) -> None:
        try:
            super().clean()
        except IntegrityError as err:
            if "identifier" in str(err):
                raise ValidationError({"identifier": "This identifier is already in use."}) from err
            raise err


class MyAdmin(admin.ModelAdmin):
    form = MyAdminForm

And this is the error getting raised:

IntegrityError at /admin/app/my_model/add/
UNIQUE constraint failed: my_model.identifier, my_model.owner_id

That error you’re seeing is being raised after clean has been called. You would need to add the code within your clean method to check the database for a duplicate value.

It tried with the following code:

    def clean(self) -> None:

        my_objs = MyModel.objects.filter(
            identifier=self.cleaned_data["identifier"],
            # owner=self.cleaned_data["owner"], # Can't do this because this field is not in the form
        ).all()

        if my_objs is not None and self.instance.id in [ds.id for ds in my_objs]:
            raise ValidationError({"identifier": "This identifier is already in use."})

        super().clean()

But again I get:

UNIQUE constraint failed: etl_datasource.identifier, etl_datasource.owner_id

Raised by the save model:

def save_model(self, request, obj, form, change):
        assert request.user.account is not None
        obj.owner = request.user.account

        super().save_model(request, obj, form, change)

Can you post your MyModel here? That may help me understand what’s going on.

What is the purpose or reason behind the second half of this and clause?

From what you’ve posted so far, I would have expected you to be able to use something like:

if MyModel.objects.filter(identifier=self.cleaned_data['identifier']).exists():
    raise ValidationError(...)

I know we’ve cross-posted here - did you see my last edit to my previous reply?

In if my_objs is not None and self.instance.id in [ds.id for ds in my_objs] the purpose of self.instance.id in [ds.id for ds in my_objs] is to validate the instance found is the same the one we wanted to recreate. The reason is that we can have several records with the same identifier, but not with same the identifierandowner` at the same time.

This condition isn’t going to do what you think it will.

At the point in time that this form is being validated, self.instance.id would be None if this is a row being created. It’s only going to have a value if you are editing an existing row, and that condition is only going to be true if you’re editing the existing row that already has this value.

The difficulty here is that you don’t have access to the request object in the form.

To make the request object available to the form used by the admin requires two parts.

First, you would need to override the __init__ method in the form to accept the request object as a parameter, save it in self, and remove it from kwargs before calling super().__init__(...).

Then you need to modify the ModelAdmin.get_form method to create the instance of the form with passing the request as a parameter to the constructor.

Actually, this is quite expected by me. My opinion is this is one of those situations where you’ve defined requirements that are pushing the Django admin beyond what it was ever intended to do.

Normally, my response to an issue like this would be for you to read the first two paragraphs at The Django admin site | Django documentation | Django . I have made multiple comments here expressing the opinion that a very common mistake among people adopting Django is to try and use it for a lot more than it was ever intended to do. You can quickly reach a point where it becomes a lot easier to just create your own view than to make the admin do what you want.

This appears to be one of those cases.

The parent get_form function does not pass the request through to the form - that’s why you’re needing to do this. Also, I did mis-state what you need to return from get_form. That function returns a form class, not an instance of the form.

This means you’re going to need to wrap this class with another class to do the work.

I found a reasonable example at Django: Access request object from admin's form.clean() - Stack Overflow