Fixes #20221: JSON CustomField does not coerce {} to null
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run

This fix actually fixes this for all valid JSON values that evaluate to
`False` in Python when loaded and cast to bool:
`bool(json.loads(<val>))`.

- `{}`
- `[]`
- `0`
- `False`

This does not change the behavior of `()` or `""` which are both
explicitly cited as "empty" values on `JSONField`.
This commit is contained in:
Jason Novinger 2025-09-05 13:01:36 -05:00 committed by Jeremy Stretch
parent 8311f457b5
commit fcb380b5c5
3 changed files with 58 additions and 2 deletions

View File

@ -538,7 +538,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# JSON # JSON
elif self.type == CustomFieldTypeChoices.TYPE_JSON: elif self.type == CustomFieldTypeChoices.TYPE_JSON:
field = JSONField(required=required, initial=json.dumps(initial) if initial else None) field = JSONField(required=required, initial=json.dumps(initial) if initial is not None else None)
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

View File

@ -1,7 +1,9 @@
import datetime import datetime
import json
from decimal import Decimal from decimal import Decimal
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import tag
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
@ -269,6 +271,60 @@ class CustomFieldTest(TestCase):
instance.refresh_from_db() instance.refresh_from_db()
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
@tag('regression')
def test_json_field_falsy_defaults(self):
"""Test that falsy JSON default values are properly handled"""
falsy_test_cases = [
({}, 'empty_dict'),
([], 'empty_array'),
(0, 'zero'),
(False, 'false_bool'),
("", 'empty_string'),
]
for default, suffix in falsy_test_cases:
with self.subTest(default=default, suffix=suffix):
cf = CustomField.objects.create(
name=f'json_falsy_{suffix}',
type=CustomFieldTypeChoices.TYPE_JSON,
default=default,
required=False
)
cf.object_types.set([self.object_type])
instance = Site.objects.create(name=f'Test Site {suffix}', slug=f'test-site-{suffix}')
self.assertIsNotNone(instance.custom_field_data)
self.assertIn(cf.name, instance.custom_field_data)
instance.refresh_from_db()
stored = instance.custom_field_data[cf.name]
self.assertEqual(stored, default)
@tag('regression')
def test_json_field_falsy_to_form_field(self):
"""Test form field generation preserves falsy defaults"""
falsy_test_cases = (
({}, json.dumps({}), 'empty_dict'),
([], json.dumps([]), 'empty_array'),
(0, json.dumps(0), 'zero'),
(False, json.dumps(False), 'false_bool'),
("", '""', 'empty_string'),
)
for default, expected, suffix in falsy_test_cases:
with self.subTest(default=default, expected=expected, suffix=suffix):
cf = CustomField.objects.create(
name=f'json_falsy_{suffix}',
type=CustomFieldTypeChoices.TYPE_JSON,
default=default,
required=False
)
cf.object_types.set([self.object_type])
form_field = cf.to_form_field(set_initial=True)
self.assertEqual(form_field.initial, expected)
def test_select_field(self): def test_select_field(self):
CHOICES = ( CHOICES = (
('a', 'Option A'), ('a', 'Option A'),

View File

@ -14,7 +14,7 @@
{{ value|isodatetime }} {{ value|isodatetime }}
{% elif customfield.type == 'url' and value %} {% elif customfield.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a> <a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif customfield.type == 'json' and value %} {% elif customfield.type == 'json' and value is not None %}
<pre>{{ value|json }}</pre> <pre>{{ value|json }}</pre>
{% elif customfield.type == 'multiselect' and value %} {% elif customfield.type == 'multiselect' and value %}
{{ value|join:", " }} {{ value|join:", " }}