From e864520821f5cbc41f8314178256ac6a793a7138 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 5 Sep 2025 13:01:36 -0500 Subject: [PATCH] Fixes #20221: JSON CustomField does not coerce `{}` to null 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())`. - `{}` - `[]` - `0` - `False` This does not change the behavior of `()` or `""` which are both explicitly cited as "empty" values on `JSONField`. --- netbox/extras/models/customfields.py | 2 +- netbox/extras/tests/test_customfields.py | 56 +++++++++++++++++++ .../templates/builtins/customfield_value.html | 2 +- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 33ddc16ac..b1d22ee0b 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -538,7 +538,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # 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 elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index c3074aa41..04e30aa0c 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,7 +1,9 @@ import datetime +import json from decimal import Decimal from django.core.exceptions import ValidationError +from django.test import tag from django.urls import reverse from rest_framework import status @@ -269,6 +271,60 @@ class CustomFieldTest(TestCase): instance.refresh_from_db() 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): CHOICES = ( ('a', 'Option A'), diff --git a/netbox/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index dbf10e1bf..fd78146a6 100644 --- a/netbox/utilities/templates/builtins/customfield_value.html +++ b/netbox/utilities/templates/builtins/customfield_value.html @@ -14,7 +14,7 @@ {{ value|isodatetime }} {% elif customfield.type == 'url' and value %} {{ value|truncatechars:70 }} -{% elif customfield.type == 'json' and value %} +{% elif customfield.type == 'json' and value is not None %}
{{ value|json }}
{% elif customfield.type == 'multiselect' and value %} {{ value|join:", " }}