#36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
--------------------------+-----------------------------------------
Reporter: theomega | Type: Uncategorized
Status: new | Component: Uncategorized
Version: 5.2 | Severity: Normal
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
--------------------------+-----------------------------------------
Using a CompositePrimaryKey on a model used in an InlineAdmin fails on
save with an `JSONDecodeError`. Maybe this is related to using a UUIDField
for the parts of the composites?
Minimal Reproducible Example:
Models:
{{{
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4,
editable=False)
name = models.CharField(max_length=255)
class Role(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4,
editable=False)
name = models.CharField(max_length=255)
class UserRole(models.Model):
pk = models.CompositePrimaryKey("user", "role")
user = models.ForeignKey(User, on_delete=models.CASCADE)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
}}}
Admin
{{{
from django.contrib import admin
from .models import User, Role, UserRole
class UserRoleInline(admin.TabularInline):
model = UserRole
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
inlines = [UserRoleInline]
# Not part of the bug, only required for creating an initial Role
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
pass
}}}
How to reproduce:
- Create a Role using the `RoleAdmin`
- Create a User using the `UserAdmin` and associcate it with a role
(works!)
- Edit the User again using the `UserAdmin` and click on save (no need to
change anything)
Expected:
- Model is saved
Instead:
- Exception is thrown:
{{{
Environment:
Request Method: POST
Request URL:
http://127.0.0.1:8181/admin/polls/user/9a232a03-dfa8-4e9f-
afcd-bd380aa0a396/change/
Django Version: 5.2
Python Version: 3.13.3
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'polls']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'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']
Traceback (most recent call last):
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args,
**callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/contrib/admin/options.py", line 719, in wrapper
return self.admin_site.admin_view(view)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/decorators.py", line 192, in _view_wrapper
result = _process_exception(request, e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/decorators.py", line 190, in _view_wrapper
response = view_func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/views/decorators/cache.py", line 80, in _view_wrapper
response = view_func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/contrib/admin/sites.py", line 246, in inner
return view(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/contrib/admin/options.py", line 1987, in change_view
return self.changeform_view(request, object_id, form_url,
extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/decorators.py", line 48, in _wrapper
return bound_method(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/decorators.py", line 192, in _view_wrapper
result = _process_exception(request, e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/decorators.py", line 190, in _view_wrapper
response = view_func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/contrib/admin/options.py", line 1843, in changeform_view
return self._changeform_view(request, object_id, form_url,
extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/contrib/admin/options.py", line 1893, in _changeform_view
if all_valid(formsets) and form_validated:
^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/formsets.py", line 584, in all_valid
return all([formset.is_valid() for formset in formsets])
^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/formsets.py", line 384, in is_valid
self.errors
^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/formsets.py", line 366, in errors
self.full_clean()
^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/formsets.py", line 423, in full_clean
for i, form in enumerate(self.forms):
^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/utils/functional.py", line 47, in __get__
res = instance.__dict__[
self.name] = self.func(instance)
^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/formsets.py", line 206, in forms
self._construct_form(i, **self.get_form_kwargs(i))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/models.py", line 1126, in _construct_form
form = super()._construct_form(i, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/forms/models.py", line 728, in _construct_form
pk = to_python(pk)
^^^^^^^^^^^^^
File "/Users/dominik/Library/Caches/pypoetry/virtualenvs/django-
composite-key-bug-VYP1oUgo-py3.13/lib/python3.13/site-
packages/django/db/models/fields/composite.py", line 151, in to_python
vals = json.loads(value)
^^^^^^^^^^^^^^^^^
File
"/opt/homebrew/Cellar/pyt...@3.13/3.13.3/Frameworks/Python.framework/Versions/3.13/lib/python3.13/json/__init__.py",
line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/opt/homebrew/Cellar/pyt...@3.13/3.13.3/Frameworks/Python.framework/Versions/3.13/lib/python3.13/json/decoder.py",
line 345, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/opt/homebrew/Cellar/pyt...@3.13/3.13.3/Frameworks/Python.framework/Versions/3.13/lib/python3.13/json/decoder.py",
line 363, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Exception Type: JSONDecodeError at /admin/polls/user/9a232a03-dfa8-4e9f-
afcd-bd380aa0a396/change/
Exception Value: Expecting value: line 1 column 1 (char 0)
}}}
I created a Github Repo reproducing this issue:
https://github.com/theomega/django_composite_key_bug
Versions:
- django 5.2
- Python 3.13.3
Looking at the variables, the `value` variable in the `to_python` has the
value of a tuple(?)
{{{
("(UUID('9a232a03-dfa8-4e9f-afcd-bd380aa0a396'), "
"UUID('03f99517-aff2-47b6-9589-e820641229df'))")
}}}
I don't think it really is a tuple, because the line before does an
isinstance check for `str`. But, anyway this is not valid JSON, so this is
why the decoder fails.
--
Ticket URL: <
https://code.djangoproject.com/ticket/36351>
Django <
https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.