#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':
'CENS...@ANOTHERCENSORED.DOMAIN', '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.