[Django] #36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError

10 views
Skip to first unread message

Django

unread,
Apr 24, 2025, 4:26:33 AM4/24/25
to django-...@googlegroups.com
#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.

Django

unread,
Apr 24, 2025, 10:12:51 AM4/24/25
to django-...@googlegroups.com
#36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
-------------------------------+--------------------------------------
Reporter: Dominik Bruhn | Owner: (none)
Type: Uncategorized | Status: closed
Component: Uncategorized | Version: 5.2
Severity: Normal | Resolution: duplicate
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------+--------------------------------------
Changes (by Simon Charette):

* resolution: => duplicate
* status: new => closed

Comment:

As pointed out [https://docs.djangoproject.com/en/5.2/topics/composite-
primary-key/#:~:text=Composite%20primary%20keys in the docs] admin support
is still in progress

> We’re still working on composite primary key support for relational
fields, including GenericForeignKey fields, and **the Django admin**.

Closing as duplicate of #35953 (''Add composite PK admin support'')
--
Ticket URL: <https://code.djangoproject.com/ticket/36351#comment:1>

Django

unread,
Apr 24, 2025, 10:17:05 AM4/24/25
to django-...@googlegroups.com
#36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
-------------------------------+--------------------------------------
Reporter: Dominik Bruhn | Owner: (none)
Type: Uncategorized | Status: closed
Component: Uncategorized | Version: 5.2
Severity: Normal | Resolution: duplicate
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------+--------------------------------------
Comment (by Dominik Bruhn):

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.
--
Ticket URL: <https://code.djangoproject.com/ticket/36351#comment:2>

Django

unread,
Apr 24, 2025, 10:31:20 AM4/24/25
to django-...@googlegroups.com
#36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
-------------------------------+--------------------------------------
Reporter: Dominik Bruhn | Owner: (none)
Type: Uncategorized | Status: closed
Component: Uncategorized | Version: 5.2
Severity: Normal | Resolution: duplicate
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------+--------------------------------------
Comment (by Natalia Bidart):

Hello Dominik Bruhn, thank you for the ticket. I think there are a couple
of points to consider:
1. As Simon mentioned, composite primary keys are not yet supported in the
admin.
2. 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
[https://csirmazbendeguz.github.io/2025/04/15/you-dont-need-composite-
primary-keys.html 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.
--
Ticket URL: <https://code.djangoproject.com/ticket/36351#comment:3>

Django

unread,
Apr 24, 2025, 10:52:31 AM4/24/25
to django-...@googlegroups.com
#36351: CompositePrimaryKey fails in InlineAdmins with a JSONDecodeError
-------------------------------+--------------------------------------
Reporter: Dominik Bruhn | Owner: (none)
Type: Uncategorized | Status: closed
Component: Uncategorized | Version: 5.2
Severity: Normal | Resolution: duplicate
Keywords: | Triage Stage: Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------+--------------------------------------
Comment (by Csirmaz Bendegúz):

we could raise an exception if `inlines` is used, similar to how we raise
an exception here:

https://github.com/django/django/blob/0596263c3136bc26cffa670e5322bd0aa56c4d34/django/contrib/admin/sites.py#L117

otherwise, yes this needs to wait for #35953
--
Ticket URL: <https://code.djangoproject.com/ticket/36351#comment:4>
Reply all
Reply to author
Forward
0 new messages