#36351 closed Uncategorized (duplicate)
CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
Reported by: | Dominik Bruhn | Owned by: | |
---|---|---|---|
Component: | Uncategorized | Version: | 5.2 |
Severity: | Normal | Keywords: | |
Cc: | Dominik Bruhn | Triage Stage: | Unreviewed |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
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/python@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/python@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/python@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.
Change History (4)
comment:1 by , 5 months ago
Resolution: | → duplicate |
---|---|
Status: | new → closed |
comment:2 by , 5 months ago
Ok, missed this, sorry for this.
I actually assume that the django.forms module also does not handle the CompositePrimaryKeys well as the error seems somewhere in this module.
comment:3 by , 5 months ago
Hello Dominik Bruhn, thank you for the ticket. I think there are a couple of points to consider:
- As Simon mentioned, composite primary keys are not yet supported in the admin.
- Looking at your models (I understand they’re a simplification), it appears the composite primary key is being used primarily as a unique constraint. If that's the case, even with more complex models, composite primary keys may not be the best fit. Django provides robust tools for defining various constraints that are often more appropriate and better supported.
If you haven’t already, I recommend reading this blog post by one of the composite PK feature authors, it offers helpful perspective on when composite primary keys are (and aren't) the right tool.
comment:4 by , 5 months ago
we could raise an exception if inlines
is used, similar to how we raise an exception here:
otherwise, yes this needs to wait for #35953
As pointed out in the docs admin support is still in progress
Closing as duplicate of #35953 (Add composite PK admin support)