#35488: Unexpected traceback: A JSONField being in a UniqueConstraint is
handled/documented poorly
-------------------------------------+-------------------------------------
               Reporter:  Hanne Moa  |          Owner:  nobody
                   Type:             |         Status:  new
  Uncategorized                      |
              Component:             |        Version:  5.0
  Uncategorized                      |       Keywords:  unique,
               Severity:  Normal     |  UniqueConstraint, JSONField, json
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 Given a User based on AbstractUser and a model:

 {{{
 class DestinationConfig(model.Model):
     class Meta:
         constraints = [models.UniqueConstraint(fields=["user",
 "settings"], name="unique_destination_per_user")]

     user = models.ForeignKey(User, on_delete=models.CASCADE)
     settings = models.JSONField()
 }}}

 Admin like so:

 {{{
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

 class DestinationConfigInline(admin.TabularInline):
     model = DestinationConfig
     ordering = ["media", "label"]
     extra = 1

     def get_formset(self, request, obj=None, **kwargs):
         self.parent_obj = obj
         return super(DestinationConfigInline, self).get_formset(request,
 obj, **kwargs)

     def get_queryset(self, request):
         qs = super(DestinationConfigInline, self).get_queryset(request)
         return qs.filter(user=self.parent_obj)

 class UserAdmin(BaseUserAdmin):
     inlines = [DestinationConfigInline]

 admin.site.register(User, UserAdmin)
 }}}

 Then attempting to save a change to a specific user leads to a traceback
 like this:

 {{{
 Environment:


 Request Method: POST
 Request URL: https://ACENSORED.DOMAIN/admin/APP_auth/user/65/change/

 Django Version: 5.0.6
 Python Version: 3.10.12
 Installed Applications:
 ['channels',
  'django.contrib.admin',
  'django.contrib.auth',
  'django.contrib.contenttypes',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
  'corsheaders',
  'social_django',
  'rest_framework',
  'rest_framework.authtoken',
  'drf_spectacular',
  'django_filters',
  'phonenumber_field',
  'argus.auth',
  'argus.incident',
  'argus.ws',
  'argus.notificationprofile',
  'argus.dev']
 Installed Middleware:
 ['django.middleware.security.SecurityMiddleware',
  'corsheaders.middleware.CorsMiddleware',
  'whitenoise.middleware.WhiteNoiseMiddleware',
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.middleware.common.CommonMiddleware',
  'django.middleware.csrf.CsrfViewMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  'django.contrib.messages.middleware.MessageMiddleware',
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
  'social_django.middleware.SocialAuthExceptionMiddleware',
  'django.contrib.auth.middleware.RemoteUserMiddleware']

 Traceback (most recent call last):
   File "/usr/local/lib/python3.10/site-
 packages/django/core/handlers/exception.py", line 55, in inner
     response = get_response(request)
   File "/usr/local/lib/python3.10/site-
 packages/django/core/handlers/base.py", line 197, in _get_response
     response = wrapped_callback(request, *callback_args,
 **callback_kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 688, in wrapper
     return self.admin_site.admin_view(view)(*args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 134, in _wrapper_view
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/sites.py", line 242, in inner
     return view(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1889, in change_view
     return self.changeform_view(request, object_id, form_url,
 extra_context)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 46, in _wrapper
     return bound_method(*args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/utils/decorators.py", line 134, in _wrapper_view
     response = view_func(request, *args, **kwargs)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1747, in changeform_view
     return self._changeform_view(request, object_id, form_url,
 extra_context)
   File "/usr/local/lib/python3.10/site-
 packages/django/contrib/admin/options.py", line 1797, in _changeform_view
     if all_valid(formsets) and form_validated:
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 579, in all_valid
     return all([formset.is_valid() for formset in formsets])
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 579, in <listcomp>
     return all([formset.is_valid() for formset in formsets])
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 384, in is_valid
     self.errors
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 366, in errors
     self.full_clean()
   File "/usr/local/lib/python3.10/site-packages/django/forms/formsets.py",
 line 456, in full_clean
     self.clean()
   File "/usr/local/lib/python3.10/site-packages/django/forms/models.py",
 line 789, in clean
     self.validate_unique()
   File "/usr/local/lib/python3.10/site-packages/django/forms/models.py",
 line 831, in validate_unique
     if row_data in seen_data:

 Exception Type: TypeError at /admin/APP_auth/user/65/change/
 Exception Value: unhashable type: 'dict'
 }}}

 * `row_data` was `('user', {'email_address':
 '[email protected]', 'html': True})`
 * `seen_data` was `set()`.

 Is it a design decision that JSONFields cannot be unique/in
 UniqueConstraints?

 * If yes, can it be documented, preferably with a note as to what to do if
 you *do* want a unique JSONField?
 * If no, can validate_unique be changed to work in this instance?

 This has also been tested on:

 * Django Version: 4.2.11
 * Python Version: 3.10.14
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35488>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107018fc3bd4365-6224dfb0-6885-4d88-8271-29fad94e57ad-000000%40eu-central-1.amazonses.com.

Reply via email to