Opened 5 months ago

Closed 5 months ago

Last modified 5 months ago

#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 Simon Charette, 5 months ago

Resolution: duplicate
Status: newclosed

As pointed out 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)

comment:2 by Dominik Bruhn, 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 Natalia Bidart, 5 months ago

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 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 Csirmaz Bendegúz, 5 months ago

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

Note: See TracTickets for help on using tickets.
Back to Top