# Customizing ValidationError message on UniqueConstraint

**URL:** https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105
**Category:** Using the ORM
**Created:** [November 7, 2023, 9:41am UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105 "2023-11-07T09:41:55Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![dstav](https://avatars.discourse-cdn.com/v4/letter/d/eb8c5e/32.png) [@dstav](https://forum.djangoproject.com/u/dstav)
#### Post date: [November 7, 2023, 9:41am UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105/1 "2023-11-07T09:41:55Z")

</div>

Assuming the following model:

```auto
class Book(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    name = models.CharField(max_length=128)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "owner"],
                name="book_unique_name_owner",
                violation_error_message="Book with this name already exists.",
            ),
        ]

```

If we create a `Book` instance that violates this constraint, calling `full_clean()` on the instance will raise ValidatioError. For example:

```auto
first_book = Book(name="A brief history of time", owner__id=1)
first_book.save()

# Try to create another book that violates the constraint
second_book = Book(name="A brief history of time", owner__id=1)
second_book.full_clean()

# django.core.exceptions.ValidationError: {' __all__': ['Book with this Name and Owner already exists.']}

```

The ValidationError is correctly raised, but the message is not the custom defined one. The relevant [documentation](https://docs.djangoproject.com/en/4.2/ref/models/constraints/#django.db.models.UniqueConstraint.violation_error_message) states:

> This message is _not used_ for [`UniqueConstraint`](https://docs.djangoproject.com/en/4.2/ref/models/constraints/#django.db.models.UniqueConstraint)s with [`fields`](https://docs.djangoproject.com/en/4.2/ref/models/constraints/#django.db.models.UniqueConstraint.fields) and without a [`condition`](https://docs.djangoproject.com/en/4.2/ref/models/constraints/#django.db.models.UniqueConstraint.condition). Such [`UniqueConstraint`](https://docs.djangoproject.com/en/4.2/ref/models/constraints/#django.db.models.UniqueConstraint)s show the same message as constraints defined with [`Field.unique`](https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.Field.unique) or in [`Meta.unique_together`](https://docs.djangoproject.com/en/4.2/ref/models/options/#django.db.models.Options.constraints).

which explains why the custom message it not displayed. It is not clear to me why the message cannot be set if there is no condition defined, but it is what it is.

I was wondering if there is a clean way to do this, or what other process to follow to achieve something similar. I believe validating the constraints as part of [object validation](https://docs.djangoproject.com/en/4.1/ref/models/instances/#validating-objects) was one of the best features added in django 4.1, which personally helped me remove a lot of redundant code from forms. But this limitation of customizing the message is a bit limiting.

Thanks.

---

<div class="post-metadata">

### Author: ![charettes](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/charettes/32/27_2.png) [@charettes](https://forum.djangoproject.com/u/charettes)
#### Post date: [November 8, 2023, 1:47am UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105/2 "2023-11-08T01:47:34Z")

</div>

Put out plainly I think this is bug in how we tried to be backward compatible.

[When constraint validation support was added](https://github.com/django/django/pull/14625) we tried to be as backward compatible as possible but I think we failed to account for the fact that passing `validation_error_message` is a good way to opt-in into the new behaviour.

I’d encourage you to [file a new bug report](https://docs.djangoproject.com/en/4.2/internals/contributing/bugs-and-features/#reporting-bugs) and even submit a PR to get this addressed. I think the following patch should get you started.

```diff
diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py
index 56d547e6b0..b7c4186712 100644
--- a/django/db/models/constraints.py
+++ b/django/db/models/constraints.py
@@ -444,14 +444,17 @@ def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
                         self.get_violation_error_message(),
                         code=self.violation_error_code,
                     )
- # When fields are defined, use the unique_error_message() for
- # backward compatibility.
- for model, constraints in instance.get_constraints():
- for constraint in constraints:
- if constraint is self:
- raise ValidationError(
- instance.unique_error_message(model, self.fields),
- )
+ if self.violation_error_message == self.default_violation_error_message:
+ # When fields are defined, use the unique_error_message() as
+ # a default for backward compatibility.
+ validation_error_message = instance.unique_error_message(model, self.fields)
+ violation_error_code = None
+ else:
+ validation_error_message = self.get_violation_error_message()
+ violation_error_code = self.violation_error_code
+ raise ValidationError(
+ validation_error_message, code=violation_error_code,
+ )
         else:
             against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
             try:
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 3ea5cf2323..5f83d2a6e2 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -31,13 +31,17 @@ class Meta:
 class UniqueConstraintProduct(models.Model):
     name = models.CharField(max_length=255)
     color = models.CharField(max_length=32, null=True)
+ age = models.IntegerField(null=True)

     class Meta:
         constraints = [
             models.UniqueConstraint(
                 fields=["name", "color"],
                 name="name_color_uniq",
- # Custom message and error code are ignored.
+ ),
+ models.UniqueConstraint(
+ fields=["color", "age"],
+ name="color_age_uniq",
                 violation_error_code="custom_code",
                 violation_error_message="Custom message",
             )
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 55df5975de..ff8c516e5f 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -369,7 +369,7 @@ def test_validate_pk_field(self):
 class UniqueConstraintTests(TestCase):
     @classmethod
     def setUpTestData(cls):
- cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red")
+ cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red", age=42)
         cls.p2 = UniqueConstraintProduct.objects.create(name="p2")

     def test_eq(self):
@@ -790,6 +790,12 @@ def test_model_validation(self):
             UniqueConstraintProduct(
                 name=self.p1.name, color=self.p1.color
             ).validate_constraints()
+ msg = "Custom message"
+ with self.assertRaisesMessage(ValidationError, msg) as cm:
+ UniqueConstraintProduct(
+ color=self.p1.color, age=self.p1.age,
+ ).validate_constraints()
+ self.assertEqual(cm.exception.error_dict[' __all__'][0].code, "custom_code")

     @skipUnlessDBFeature("supports_partial_indexes")
     def test_model_validation_with_condition(self):
@@ -827,6 +833,14 @@ class Meta:
             NoCodeErrorConstraintModel(name="test").validate_constraints()

     def test_validate(self):
+ constraint = UniqueConstraintProduct._meta.constraints[1]
+ msg = "Custom message"
+ non_unique_product = UniqueConstraintProduct(
+ color=self.p1.color, age=self.p1.age,
+ )
+ with self.assertRaisesMessage(ValidationError, msg) as cm:
+ constraint.validate(UniqueConstraintProduct, non_unique_product)
+ self.assertEqual(cm.exception.code, "custom_code")
         constraint = UniqueConstraintProduct._meta.constraints[0]
         msg = "Unique constraint product with this Name and Color already exists."
         non_unique_product = UniqueConstraintProduct(

```

---

<div class="post-metadata">

### Author: ![dstav](https://avatars.discourse-cdn.com/v4/letter/d/eb8c5e/32.png) [@dstav](https://forum.djangoproject.com/u/dstav)
#### Post date: [November 9, 2023, 8:32pm UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105/3 "2023-11-09T20:32:10Z")

</div>

That is good to know, thank you for the insight!

I will go ahead and open a bug report as you suggest. I will take a close look at the patch, thank you.

---

<div class="post-metadata">

### Author: ![gabn88](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gabn88/32/18321_2.png) [@gabn88](https://forum.djangoproject.com/u/gabn88)
#### Post date: [January 11, 2024, 2:44pm UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105/4 "2024-01-11T14:44:53Z")

</div>

See [#35103 (UniqueConstraint message does not use violation\_error\_message) – Django](https://code.djangoproject.com/ticket/35103) for the bug report. Thanks Simon for this response and the good starting point for a patch!

---

<div class="post-metadata">

### Author: ![AlexT](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/alext/32/32582_2.png) [@AlexT](https://forum.djangoproject.com/u/AlexT)
#### Post date: [December 18, 2024, 6:48pm UTC](https://forum.djangoproject.com/t/customizing-validationerror-message-on-uniqueconstraint/25105/5 "2024-12-18T18:48:56Z")

</div>

Just as an update for anyone looking (I wanted this one today!) - this was finally merged into main back in October, so should be in Django 5.2.

> <https://github.com/django/django/pull/17723>
>
> ticket-35103
> 
> Adds a custom validation message and code to a UniqueConstraint… when it is set (and not only on conditional constraints).
