diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py
index bae451912..8ebc5192f 100644
--- a/openwisp_controller/config/admin.py
+++ b/openwisp_controller/config/admin.py
@@ -15,6 +15,7 @@
ObjectDoesNotExist,
ValidationError,
)
+from django.db import models
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.http.response import HttpResponseForbidden
from django.shortcuts import get_object_or_404
@@ -217,6 +218,9 @@ def _get_preview_instance(self, request):
key = "{relation}_id".format(relation=key)
# pass non-empty string or None
kwargs[key] = value or None
+ # parse JSON strings for JSONField fields
+ elif isinstance(field, models.JSONField) and isinstance(value, str):
+ kwargs[key] = json.loads(value) if value else None
# put regular field values in kwargs dict
else:
kwargs[key] = value
diff --git a/openwisp_controller/config/base/base.py b/openwisp_controller/config/base/base.py
index 8b55ad019..a3f579388 100644
--- a/openwisp_controller/config/base/base.py
+++ b/openwisp_controller/config/base/base.py
@@ -1,4 +1,3 @@
-import collections
import hashlib
import json
import logging
@@ -6,11 +5,12 @@
from cache_memoize import cache_memoize
from django.core.exceptions import ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
+from django.db.models import JSONField
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from netjsonconfig.exceptions import ValidationError as SchemaError
from openwisp_utils.base import TimeStampedEditableModel
@@ -126,8 +126,7 @@ class BaseConfig(BaseModel):
_("configuration"),
default=dict,
help_text=_("configuration in NetJSON DeviceConfiguration format"),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
__template__ = False
diff --git a/openwisp_controller/config/base/config.py b/openwisp_controller/config/base/config.py
index efb42ff65..855d94541 100644
--- a/openwisp_controller/config/base/config.py
+++ b/openwisp_controller/config/base/config.py
@@ -6,9 +6,10 @@
from cache_memoize import cache_memoize
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
+from django.db.models import JSONField
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from model_utils import Choices
from model_utils.fields import StatusField
from netjsonconfig import OpenWrt
@@ -90,8 +91,7 @@ class AbstractConfig(ChecksumCacheMixin, BaseConfig):
'" target="_blank">'
"configuration variables"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
checksum_db = models.CharField(
_("configuration checksum"),
diff --git a/openwisp_controller/config/base/device_group.py b/openwisp_controller/config/base/device_group.py
index 366d61667..d7f6be31a 100644
--- a/openwisp_controller/config/base/device_group.py
+++ b/openwisp_controller/config/base/device_group.py
@@ -1,11 +1,11 @@
-import collections
from copy import deepcopy
import jsonschema
from django.core.exceptions import ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
+from django.db.models import JSONField
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from jsonschema.exceptions import ValidationError as SchemaError
from swapper import get_model_name, load_model
@@ -39,24 +39,22 @@ class AbstractDeviceGroup(OrgMixin, TimeStampedEditableModel):
meta_data = JSONField(
blank=True,
default=dict,
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
help_text=_(
"Store custom metadata related to this group. This field is intended "
"for arbitrary data that does not affect device configuration and can "
"be retrieved via the REST API for integrations or external tools."
),
verbose_name=_("Metadata"),
+ encoder=DjangoJSONEncoder,
)
context = JSONField(
blank=True,
default=dict,
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
help_text=_(
"Define configuration variables available to all devices in this group"
),
verbose_name=_("Configuration Variables"),
+ encoder=DjangoJSONEncoder,
)
def __str__(self):
diff --git a/openwisp_controller/config/base/multitenancy.py b/openwisp_controller/config/base/multitenancy.py
index 07c34bb68..67b96761e 100644
--- a/openwisp_controller/config/base/multitenancy.py
+++ b/openwisp_controller/config/base/multitenancy.py
@@ -1,11 +1,11 @@
-import collections
from copy import deepcopy
import swapper
from django.core.exceptions import ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
+from django.db.models import JSONField
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from openwisp_utils.base import KeyField, UUIDModel
from openwisp_utils.fields import FallbackBooleanChoiceField
@@ -42,13 +42,12 @@ class AbstractOrganizationConfigSettings(UUIDModel):
context = JSONField(
blank=True,
default=dict,
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
help_text=_(
"Define reusable configuration variables available "
"to all devices in this organization"
),
verbose_name=_("Configuration Variables"),
+ encoder=DjangoJSONEncoder,
)
class Meta:
diff --git a/openwisp_controller/config/base/template.py b/openwisp_controller/config/base/template.py
index 25e89eee3..555366b7b 100644
--- a/openwisp_controller/config/base/template.py
+++ b/openwisp_controller/config/base/template.py
@@ -4,9 +4,10 @@
from copy import copy
from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
+from django.db.models import JSONField
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from netjsonconfig.exceptions import ValidationError as NetjsonconfigValidationError
from swapper import get_model_name, load_model
from taggit.managers import TaggableManager
@@ -101,8 +102,7 @@ class AbstractTemplate(ShareableOrgMixinUniqueName, BaseConfig):
"These values are used during validation and when a variable is "
"not provided by the device, group, or organization."
),
- load_kwargs={"object_pairs_hook": OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
__template__ = True
diff --git a/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py b/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
index 274fe2d44..0121967ee 100644
--- a/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
+++ b/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-03 17:54
-import collections
import re
import uuid
import django.core.validators
import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
@@ -58,12 +56,10 @@ class Migration(migrations.Migration):
),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
@@ -250,12 +246,10 @@ class Migration(migrations.Migration):
),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
@@ -347,11 +341,9 @@ class Migration(migrations.Migration):
("name", models.CharField(max_length=64, unique=True)),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
diff --git a/openwisp_controller/config/migrations/0018_config_context.py b/openwisp_controller/config/migrations/0018_config_context.py
index 5b897cde9..9aa0e5156 100644
--- a/openwisp_controller/config/migrations/0018_config_context.py
+++ b/openwisp_controller/config/migrations/0018_config_context.py
@@ -1,9 +1,7 @@
# Generated by Django 2.1.4 on 2019-01-09 01:52
-import collections
-import jsonfield.fields
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,15 +11,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="config",
name="context",
- field=jsonfield.fields.JSONField(
+ field=models.JSONField(
blank=True,
- dump_kwargs={"indent": 4},
help_text=(
'Additional context '
"(configuration variables) in JSON format"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
null=True,
),
)
diff --git a/openwisp_controller/config/migrations/0023_update_context.py b/openwisp_controller/config/migrations/0023_update_context.py
index 68edd30da..49c4cb850 100644
--- a/openwisp_controller/config/migrations/0023_update_context.py
+++ b/openwisp_controller/config/migrations/0023_update_context.py
@@ -1,9 +1,7 @@
# Generated by Django 3.0.3 on 2020-02-26 19:58
-import collections
-import jsonfield.fields
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,17 +11,15 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="config",
name="context",
- field=jsonfield.fields.JSONField(
+ field=models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"allows overriding "
''
"configuration variables"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
),
)
]
diff --git a/openwisp_controller/config/migrations/0028_template_default_values.py b/openwisp_controller/config/migrations/0028_template_default_values.py
index 13651f4dd..f67cca27e 100644
--- a/openwisp_controller/config/migrations/0028_template_default_values.py
+++ b/openwisp_controller/config/migrations/0028_template_default_values.py
@@ -1,9 +1,7 @@
# Generated by Django 3.0.3 on 2020-06-29 23:54
-import collections
-import jsonfield.fields
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,16 +11,14 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="template",
name="default_values",
- field=jsonfield.fields.JSONField(
+ field=models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"Define default values for the variables used in this template. "
"These values are used during validation and when a variable is "
"not provided by the device, group, or organization."
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Default Values",
),
)
diff --git a/openwisp_controller/config/migrations/0036_device_group.py b/openwisp_controller/config/migrations/0036_device_group.py
index f3ca39ced..af180d116 100644
--- a/openwisp_controller/config/migrations/0036_device_group.py
+++ b/openwisp_controller/config/migrations/0036_device_group.py
@@ -1,11 +1,9 @@
# Generated by Django 3.1.12 on 2021-06-14 17:51
-import collections
import uuid
import django.db.models.deletion
import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
import swapper
from django.db import migrations, models
@@ -56,11 +54,9 @@ class Migration(migrations.Migration):
),
(
"meta_data",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
help_text=(
"Store custom metadata related to this group. "
"This field is intended for arbitrary data that "
diff --git a/openwisp_controller/config/migrations/0049_devicegroup_context.py b/openwisp_controller/config/migrations/0049_devicegroup_context.py
index 34733f4db..a085c908c 100644
--- a/openwisp_controller/config/migrations/0049_devicegroup_context.py
+++ b/openwisp_controller/config/migrations/0049_devicegroup_context.py
@@ -1,9 +1,7 @@
# Generated by Django 3.2.19 on 2023-06-27 14:45
-import collections
-import jsonfield.fields
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -15,15 +13,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="devicegroup",
name="context",
- field=jsonfield.fields.JSONField(
+ field=models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"Define configuration variables available "
"to all devices in this group"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
),
),
diff --git a/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py b/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
index b643e2e12..e6de53839 100644
--- a/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
+++ b/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
@@ -1,9 +1,7 @@
# Generated by Django 3.2.20 on 2023-07-22 10:49
-import collections
-import jsonfield.fields
-from django.db import migrations
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -15,15 +13,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="organizationconfigsettings",
name="context",
- field=jsonfield.fields.JSONField(
+ field=models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"indent": 4},
help_text=(
"Define reusable configuration variables available "
"to all devices in this organization"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
),
),
diff --git a/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py b/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py
new file mode 100644
index 000000000..d985a5af3
--- /dev/null
+++ b/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py
@@ -0,0 +1,125 @@
+# Generated by Django 5.2.12 on 2026-04-02 06:56
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "config",
+ "0063_organizationconfigsettings_estimated_location_enabled_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="config",
+ name="config",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="config",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "allows overriding "
+ ''
+ "configuration variables"
+ ),
+ ),
+ ),
+ migrations.AlterField(
+ model_name="devicegroup",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define configuration variables available "
+ "to all devices in this group"
+ ),
+ verbose_name="Configuration Variables",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="devicegroup",
+ name="meta_data",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Store custom metadata related to this group. "
+ "This field is intended for arbitrary data that "
+ "does not affect device configuration and can be "
+ "retrieved via the REST API for integrations or "
+ "external tools."
+ ),
+ verbose_name="Metadata",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organizationconfigsettings",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define reusable configuration variables "
+ "available to all devices in this organization"
+ ),
+ verbose_name="Configuration Variables",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="template",
+ name="config",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="template",
+ name="default_values",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define default values for the variables used "
+ "in this template. These values are used during "
+ "validation and when a variable is not provided "
+ "by the device, group, or organization."
+ ),
+ verbose_name="Default Values",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="vpn",
+ name="config",
+ field=models.JSONField(
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ ]
diff --git a/openwisp_controller/config/tests/test_api.py b/openwisp_controller/config/tests/test_api.py
index 516b67fc3..6fcf9c527 100644
--- a/openwisp_controller/config/tests/test_api.py
+++ b/openwisp_controller/config/tests/test_api.py
@@ -428,8 +428,8 @@ def test_device_put_api(self):
"backend": "netjsonconfig.OpenWisp",
"status": "modified",
"templates": [],
- "context": "{}",
- "config": "{}",
+ "context": {},
+ "config": {},
},
}
r = self.client.put(path, data, content_type="application/json")
diff --git a/openwisp_controller/connection/base/models.py b/openwisp_controller/connection/base/models.py
index 15f4eff63..441bfe73f 100644
--- a/openwisp_controller/connection/base/models.py
+++ b/openwisp_controller/connection/base/models.py
@@ -1,16 +1,16 @@
-import collections
import logging
import django
import jsonschema
from django.core.exceptions import ValidationError
+from django.core.serializers.json import DjangoJSONEncoder
from django.db import models, transaction
+from django.db.models import JSONField
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
-from jsonfield import JSONField
from jsonschema.exceptions import ValidationError as SchemaError
from swapper import get_model_name, load_model
@@ -101,8 +101,7 @@ class AbstractCredentials(ConnectorMixin, ShareableOrgMixinUniqueName, BaseModel
_("parameters"),
default=dict,
help_text=_("global connection parameters"),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
auto_add = models.BooleanField(
_("auto add"),
@@ -245,8 +244,7 @@ class AbstractDeviceConnection(ConnectorMixin, TimeStampedEditableModel):
"local connection parameters (will override "
"the global parameters if specified)"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
# usability improvements
is_working = models.BooleanField(null=True, blank=True, default=None)
@@ -425,8 +423,7 @@ class AbstractCommand(TimeStampedEditableModel):
input = JSONField(
blank=True,
null=True,
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
- dump_kwargs={"indent": 4},
+ encoder=DjangoJSONEncoder,
)
output = models.TextField(blank=True)
diff --git a/openwisp_controller/connection/migrations/0001_initial.py b/openwisp_controller/connection/migrations/0001_initial.py
index 24e902931..0e5553733 100644
--- a/openwisp_controller/connection/migrations/0001_initial.py
+++ b/openwisp_controller/connection/migrations/0001_initial.py
@@ -1,11 +1,8 @@
# Generated by Django 2.0.5 on 2018-05-05 17:33
-import collections
import uuid
import django.db.models.deletion
-import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
import swapper
from django.conf import settings
@@ -70,11 +67,9 @@ class Migration(migrations.Migration):
),
(
"params",
- jsonfield.fields.JSONField(
+ models.JSONField(
default=dict,
- dump_kwargs={"indent": 4},
help_text="global connection parameters",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="parameters",
),
),
@@ -137,15 +132,13 @@ class Migration(migrations.Migration):
("enabled", models.BooleanField(db_index=True, default=True)),
(
"params",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"indent": 4},
help_text=(
"local connection parameters (will override "
"the global parameters if specified)"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="parameters",
),
),
diff --git a/openwisp_controller/connection/migrations/0007_command.py b/openwisp_controller/connection/migrations/0007_command.py
index ec04d55a1..5ec72cfac 100644
--- a/openwisp_controller/connection/migrations/0007_command.py
+++ b/openwisp_controller/connection/migrations/0007_command.py
@@ -1,13 +1,11 @@
# Generated by Django 3.1.2 on 2020-10-09 22:01
-import collections
import uuid
import django
import django.db.migrations.operations.special
import django.db.models.deletion
import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
import swapper
from django.db import migrations, models
@@ -76,10 +74,8 @@ class Migration(migrations.Migration):
),
(
"input",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
- dump_kwargs={"indent": 4},
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
null=True,
),
),
diff --git a/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py b/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
new file mode 100644
index 000000000..d83e0364a
--- /dev/null
+++ b/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
@@ -0,0 +1,47 @@
+# Generated by Django 5.2.11 on 2026-02-06 06:08
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("connection", "0009_alter_deviceconnection_unique_together"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="command",
+ name="input",
+ field=models.JSONField(
+ blank=True,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="credentials",
+ name="params",
+ field=models.JSONField(
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="global connection parameters",
+ verbose_name="parameters",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="deviceconnection",
+ name="params",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "local connection parameters (will override the global "
+ "parameters if specified)"
+ ),
+ verbose_name="parameters",
+ ),
+ ),
+ ]
diff --git a/openwisp_controller/connection/tests/test_models.py b/openwisp_controller/connection/tests/test_models.py
index 4766a3f7e..3dac7ffc6 100644
--- a/openwisp_controller/connection/tests/test_models.py
+++ b/openwisp_controller/connection/tests/test_models.py
@@ -502,7 +502,7 @@ def test_command_validation(self):
with self.subTest("test extra arg on reboot"):
command.type = "reboot"
- command.input = '["test"]'
+ command.input = ["test"]
with self.assertRaises(ValidationError) as context_manager:
command.full_clean()
e = context_manager.exception
@@ -536,12 +536,12 @@ def test_command_validation(self):
self.assertIn("input", e.message_dict)
self.assertEqual(
e.message_dict["input"],
- ["Enter valid JSON.", "'notjson' is not of type 'object'"],
+ ["'notjson' is not of type 'object'"],
)
with self.subTest("JSON check on arguments"):
command.type = "change_password"
- command.input = "[]"
+ command.input = []
with self.assertRaises(ValidationError) as context_manager:
command.full_clean()
e = context_manager.exception
@@ -601,7 +601,7 @@ def test_arguments(self):
self.assertEqual(list(command.arguments), ["newpwd", "newpwd"])
with self.subTest("value error"):
- command = Command(input='["echo test"]', type="custom")
+ command = Command(input=["echo test"], type="custom")
with self.assertRaises(TypeError) as context_manager:
command.arguments
self.assertEqual(
@@ -972,7 +972,7 @@ def _assert_applying_conf_test_command(mocked_exec):
self.assertEqual(conf.status, "modified")
with self.subTest("openwisp_config >= 0.6.0a"):
- conf.config = '{"dns_servers": []}'
+ conf.config = {"dns_servers": []}
conf.full_clean()
with mock.patch(_exec_command_path) as mocked_exec_command:
mocked_exec_command.return_value = self._exec_command_return_value(
@@ -987,7 +987,7 @@ def _assert_applying_conf_test_command(mocked_exec):
self.assertEqual(conf.status, "modified")
with self.subTest("openwisp_config < 0.6.0a: exit_code 0"):
- conf.config = '{"interfaces": [{"name": "eth00","type": "ethernet"}]}'
+ conf.config = {"interfaces": [{"name": "eth00", "type": "ethernet"}]}
conf.full_clean()
with mock.patch(_exec_command_path) as mocked_exec_command:
mocked_exec_command.return_value = self._exec_command_return_value(
@@ -1001,7 +1001,7 @@ def _assert_applying_conf_test_command(mocked_exec):
self.assertEqual(conf.status, "modified")
with self.subTest("openwisp_config < 0.6.0a: exit_code 1"):
- conf.config = '{"radios": []}'
+ conf.config = {"radios": []}
conf.full_clean()
with mock.patch(_exec_command_path) as mocked_exec_command:
stdin, stdout, stderr = self._exec_command_return_value(
diff --git a/openwisp_controller/pki/migrations/0001_initial.py b/openwisp_controller/pki/migrations/0001_initial.py
index 014d050f8..5ac454a1b 100644
--- a/openwisp_controller/pki/migrations/0001_initial.py
+++ b/openwisp_controller/pki/migrations/0001_initial.py
@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-07 17:49
-import collections
import django.db.models.deletion
import django.utils.timezone
import django_x509.base.models
-import jsonfield.fields
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
@@ -111,12 +109,10 @@ class Migration(migrations.Migration):
),
(
"extensions",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=list,
- dump_kwargs={"indent": 4},
help_text="additional x509 certificate extensions",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extensions",
),
),
@@ -264,12 +260,10 @@ class Migration(migrations.Migration):
),
(
"extensions",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=list,
- dump_kwargs={"indent": 4},
help_text="additional x509 certificate extensions",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extensions",
),
),
diff --git a/requirements.txt b/requirements.txt
index 386f4b9a2..770920c71 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,5 +16,4 @@ django-cache-memoize~=0.2.1
shortuuid~=1.0.13
netaddr~=1.3.0
django-import-export~=4.3.14
-jsonfield>=3.1.0,<4.0.0
geoip2>=5.1.0,<6.0.0
diff --git a/tests/openwisp2/sample_config/migrations/0001_initial.py b/tests/openwisp2/sample_config/migrations/0001_initial.py
index c6bb22c89..3944b083e 100644
--- a/tests/openwisp2/sample_config/migrations/0001_initial.py
+++ b/tests/openwisp2/sample_config/migrations/0001_initial.py
@@ -1,12 +1,10 @@
# Generated by Django 3.0.7 on 2020-06-27 11:16
-import collections
import re
import uuid
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
import swapper
import taggit.managers
@@ -75,12 +73,10 @@ class Migration(migrations.Migration):
),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
@@ -116,17 +112,15 @@ class Migration(migrations.Migration):
),
(
"context",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"allows overriding "
''
"configuration variables"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
),
),
("details", models.CharField(blank=True, max_length=64, null=True)),
@@ -244,11 +238,9 @@ class Migration(migrations.Migration):
("name", models.CharField(db_index=True, max_length=64, unique=True)),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
@@ -510,12 +502,10 @@ class Migration(migrations.Migration):
),
(
"config",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text="configuration in NetJSON DeviceConfiguration format",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="configuration",
),
),
@@ -575,17 +565,15 @@ class Migration(migrations.Migration):
),
(
"default_values",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"Define default values for the variables used "
"in this template. These values are used during "
"validation and when a variable is not provided "
"by the device, group, or organization."
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Default Values",
),
),
@@ -706,15 +694,13 @@ class Migration(migrations.Migration):
),
(
"context",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"indent": 4},
help_text=(
"Define reusable configuration variables "
"available to all devices in this organization"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
),
),
@@ -761,11 +747,9 @@ class Migration(migrations.Migration):
),
(
"meta_data",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
help_text=(
"Store custom metadata related to this group. "
"This field is intended for arbitrary data that "
@@ -778,15 +762,13 @@ class Migration(migrations.Migration):
),
(
"context",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"Define configuration variables available "
"to all devices in this group"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
),
),
diff --git a/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py b/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py
new file mode 100644
index 000000000..dfe029cf8
--- /dev/null
+++ b/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py
@@ -0,0 +1,125 @@
+# Generated by Django 5.2.12 on 2026-04-02 07:00
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "sample_config",
+ "0009_organizationconfigsettings_approximate_location_enabled_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="config",
+ name="config",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="config",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "allows overriding "
+ ''
+ "configuration variables"
+ ),
+ ),
+ ),
+ migrations.AlterField(
+ model_name="devicegroup",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define configuration variables available "
+ "to all devices in this group"
+ ),
+ verbose_name="Configuration Variables",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="devicegroup",
+ name="meta_data",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Store custom metadata related to this group. "
+ "This field is intended for arbitrary data that "
+ "does not affect device configuration and can be "
+ "retrieved via the REST API for integrations or "
+ "external tools."
+ ),
+ verbose_name="Metadata",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="organizationconfigsettings",
+ name="context",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define reusable configuration variables "
+ "available to all devices in this organization"
+ ),
+ verbose_name="Configuration Variables",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="template",
+ name="config",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="template",
+ name="default_values",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "Define default values for the variables used "
+ "in this template. These values are used during "
+ "validation and when a variable is not provided "
+ "by the device, group, or organization."
+ ),
+ verbose_name="Default Values",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="vpn",
+ name="config",
+ field=models.JSONField(
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="configuration in NetJSON DeviceConfiguration format",
+ verbose_name="configuration",
+ ),
+ ),
+ ]
diff --git a/tests/openwisp2/sample_connection/migrations/0001_initial.py b/tests/openwisp2/sample_connection/migrations/0001_initial.py
index ab73a253d..a030b5e4f 100644
--- a/tests/openwisp2/sample_connection/migrations/0001_initial.py
+++ b/tests/openwisp2/sample_connection/migrations/0001_initial.py
@@ -1,12 +1,10 @@
# Generated by Django 3.0.6 on 2020-05-10 18:11
-import collections
import uuid
import django
import django.db.models.deletion
import django.utils.timezone
-import jsonfield.fields
import model_utils.fields
import swapper
from django.conf import settings
@@ -70,11 +68,9 @@ class Migration(migrations.Migration):
),
(
"params",
- jsonfield.fields.JSONField(
+ models.JSONField(
default=dict,
- dump_kwargs={"indent": 4},
help_text="global connection parameters",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="parameters",
),
),
@@ -155,15 +151,13 @@ class Migration(migrations.Migration):
("enabled", models.BooleanField(db_index=True, default=True)),
(
"params",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=dict,
- dump_kwargs={"indent": 4},
help_text=(
"local connection parameters (will override the "
"global parameters if specified)"
),
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="parameters",
),
),
@@ -256,10 +250,8 @@ class Migration(migrations.Migration):
),
(
"input",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
- dump_kwargs={"indent": 4},
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
null=True,
),
),
diff --git a/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py b/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
new file mode 100644
index 000000000..f74802cae
--- /dev/null
+++ b/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
@@ -0,0 +1,47 @@
+# Generated by Django 5.2.11 on 2026-02-06 06:11
+
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("sample_connection", "0003_name_unique_per_organization"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="command",
+ name="input",
+ field=models.JSONField(
+ blank=True,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="credentials",
+ name="params",
+ field=models.JSONField(
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text="global connection parameters",
+ verbose_name="parameters",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="deviceconnection",
+ name="params",
+ field=models.JSONField(
+ blank=True,
+ default=dict,
+ encoder=django.core.serializers.json.DjangoJSONEncoder,
+ help_text=(
+ "local connection parameters (will override the global "
+ "parameters if specified)"
+ ),
+ verbose_name="parameters",
+ ),
+ ),
+ ]
diff --git a/tests/openwisp2/sample_pki/migrations/0001_initial.py b/tests/openwisp2/sample_pki/migrations/0001_initial.py
index 2b7ec4020..6db6ad3db 100644
--- a/tests/openwisp2/sample_pki/migrations/0001_initial.py
+++ b/tests/openwisp2/sample_pki/migrations/0001_initial.py
@@ -1,11 +1,9 @@
# Generated by Django 3.0.6 on 2020-05-10 18:14
-import collections
import django.db.models.deletion
import django.utils.timezone
import django_x509.base.models
-import jsonfield.fields
import model_utils.fields
import swapper
from django.conf import settings
@@ -128,12 +126,10 @@ class Migration(migrations.Migration):
),
(
"extensions",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=list,
- dump_kwargs={"indent": 4},
help_text="additional x509 certificate extensions",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extensions",
),
),
@@ -306,12 +302,10 @@ class Migration(migrations.Migration):
),
(
"extensions",
- jsonfield.fields.JSONField(
+ models.JSONField(
blank=True,
default=list,
- dump_kwargs={"indent": 4},
help_text="additional x509 certificate extensions",
- load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extensions",
),
),