#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/[email protected]/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/[email protected]/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/[email protected]/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.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019666e7bf2e-88c81682-3baa-4cce-bc7c-cce42ae6cd0d-000000%40eu-central-1.amazonses.com.

Reply via email to