Skip to content

[change] Replaced thirdparty JSONField with Django built-in JSONField#1214

Open
Eeshu-Yadav wants to merge 1 commit intoopenwisp:masterfrom
Eeshu-Yadav:1061-replace-thirdparty-jsonfield-with-django-builtin1
Open

[change] Replaced thirdparty JSONField with Django built-in JSONField#1214
Eeshu-Yadav wants to merge 1 commit intoopenwisp:masterfrom
Eeshu-Yadav:1061-replace-thirdparty-jsonfield-with-django-builtin1

Conversation

@Eeshu-Yadav
Copy link

@Eeshu-Yadav Eeshu-Yadav commented Feb 4, 2026

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #1061.

Description of Changes

This PR replaces the third-party jsonfield package with Django's built-in JSONField across all OpenWISP controller models to modernize the codebase and remove dependency on an unmaintained package.

Changes Made:

  • Updated 6 model files to use django.db.models.JSONField instead of third-party jsonfield.JSONField:

    • openwisp_controller/config/base/base.py
    • openwisp_controller/config/base/config.py
    • openwisp_controller/config/base/device_group.py
    • openwisp_controller/config/base/multitenancy.py
    • openwisp_controller/config/base/template.py
    • openwisp_controller/connection/base/models.py
  • Created 4 migration files to safely alter field types:

    • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
    • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
    • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
    • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • Added DjangoJSONEncoder to all JSONField instances to handle lazy translation objects during serialization

  • Updated 20+ old migration files to use Django's JSONField and remove jsonfield-specific parameters

  • Enhanced admin preview functionality to handle JSONField data parsing from form submissions

  • Updated test cases to work with Django's JSONField behavior (passing Python objects instead of JSON strings)

  • Fixed code quality issues by removing unused imports and duplicate imports

Technical Details:

  • Django's JSONField handles JSON serialization internally without needing external parameters
  • Added encoder=DjangoJSONEncoder to prevent serialization errors with lazy translation objects (see openwisp-notifications#438)
  • The load_kwargs={"object_pairs_hook": collections.OrderedDict} is no longer needed as Python 3.7+ dicts maintain insertion order
  • The dump_kwargs={"indent": 4} formatting should be handled at the serialization/display layer, not the model level
  • Admin preview parsing code retained to convert form JSON strings to Python objects for validation
  • All existing JSON data remains compatible and accessible

Copilot AI review requested due to automatic review settings February 4, 2026 03:05
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Walkthrough

This PR replaces the external jsonfield library with Django's built-in JSONField across the codebase. Updates include converting model field definitions to use encoder=DjangoJSONEncoder instead of load_kwargs/dump_kwargs, creating data migrations to alter existing database fields, removing the jsonfield dependency from requirements, updating admin logic to handle JSON string parsing, and adjusting test data to use native Python data structures instead of JSON strings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: replacing third-party JSONField with Django's built-in JSONField.
Description check ✅ Passed The PR description comprehensively covers all required template sections with detailed information about changes, migration strategy, and technical rationale.
Linked Issues check ✅ Passed The PR successfully addresses all objectives from issue #1061: removes jsonfield dependency, adopts Django's JSONField, ensures safe migration with data preservation, and eliminates jsonfield-specific parameters.
Out of Scope Changes check ✅ Passed All changes are directly related to the migration from third-party jsonfield to Django's built-in JSONField as specified in issue #1061; no unrelated code modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3177b52 and 5a81fdf.

📒 Files selected for processing (27)
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • requirements.txt
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
💤 Files with no reviewable changes (1)
  • requirements.txt
🚧 Files skipped from review as they are similar to previous changes (10)
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/migrations/0023_update_context.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/config/base/config.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/tests/test_api.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
🧬 Code graph analysis (5)
openwisp_controller/config/base/template.py (1)
openwisp_controller/config/base/base.py (1)
  • json (276-283)
openwisp_controller/config/base/device_group.py (1)
openwisp_controller/config/base/base.py (1)
  • json (276-283)
openwisp_controller/config/base/config.py (1)
openwisp_controller/config/base/base.py (1)
  • json (276-283)
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (2)
openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-47)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (4)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
tests/openwisp2/sample_config/migrations/0001_initial.py (1)
  • Migration (22-1039)
🪛 Ruff (0.14.14)
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-117: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-117: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
🔇 Additional comments (19)
openwisp_controller/config/tests/test_api.py (1)

427-428: LGTM!

Correctly changed from double-encoded JSON strings ("{}") to native Python dicts ({}), consistent with Django's built-in JSONField expectations and with other tests in this file.

openwisp_controller/config/migrations/0028_template_default_values.py (1)

4-23: Clean migration update — looks good.

The switch from jsonfield.fields.JSONField to models.JSONField correctly removes the third-party dependency from the migration graph. The omission of encoder=DjangoJSONEncoder here is fine since migration 0062 will apply it via AlterField; the encoder parameter only affects Python-side serialization, not the DB column type, so the intermediate state is harmless.

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (2)

1-11: LGTM — Migration structure and dependency chain look correct.

The imports are minimal and necessary, and the dependency on 0009_alter_deviceconnection_unique_together properly sequences this migration.


13-46: AlterField operations are well-structured and consistent with base model definitions.

All three fields correctly use models.JSONField with encoder=DjangoJSONEncoder, which handles Django-specific types (lazy translations, datetimes, UUIDs, Decimals) that the plain json.JSONEncoder would reject. The default=dict callable avoids the mutable default pitfall. AlterField is the right operation here — the underlying JSON/JSONB column type is compatible, so this migrates cleanly without data loss. Field definitions match the base model declarations exactly.

openwisp_controller/config/base/template.py (1)

7-9: Clean migration to Django's built-in JSONField.

The imports and field definition are correct. DjangoJSONEncoder properly handles lazy translation strings and Django-specific types. The existing clean() validation at line 226–228 (checking isinstance(self.default_values, dict)) remains correct since Django's JSONField returns native Python objects.

Also applies to: 96-107

openwisp_controller/config/base/config.py (1)

9-11: LGTM!

The context field migration is correct. The existing clean() validation (lines 570–575) and get_context() (line 957) both work correctly with native dicts returned by Django's JSONField.

Also applies to: 85-95

openwisp_controller/config/migrations/0018_config_context.py (1)

4-4: Clean removal of jsonfield dependency from migration.

The field state (blank=True, null=True, no default, no encoder) is correct for this migration's point in the chain — subsequent migrations (0023, 0062) handle adding default=dict and encoder=DjangoJSONEncoder respectively.

Also applies to: 14-22

openwisp_controller/connection/migrations/0007_command.py (1)

76-81: Migration 0010 correctly covers Command.input with the encoder parameter.

Verification confirms that migration 0010_replace_jsonfield_with_django_builtin.py includes the AlterField operation for Command.input (lines 14–22) with encoder=django.core.serializers.json.DjangoJSONEncoder. This migration properly handles all three JSONField instances: Command.input, Credentials.params, and DeviceConnection.params. The pattern in migration 0007 is correct.

openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)

16-24: No action needed. Migration 0062 already includes an AlterField for organizationconfigsettings.context with encoder=django.core.serializers.json.DjangoJSONEncoder at lines 67-80, exactly as required.

openwisp_controller/config/base/device_group.py (2)

5-7: LGTM!

The import changes correctly switch from the third-party jsonfield to Django's built-in JSONField and DjangoJSONEncoder.


39-58: LGTM!

Both meta_data and context fields are correctly updated to use models.JSONField with encoder=DjangoJSONEncoder. The load_kwargs/dump_kwargs parameters have been properly removed, and the rest of the field arguments (blank, default, help_text, verbose_name) are preserved.

tests/openwisp2/sample_connection/migrations/0001_initial.py (3)

69-76: LGTM!

The params field for Credentials is correctly switched to models.JSONField. The encoder will be added by the subsequent 0004 migration.


152-163: LGTM!

The params field for DeviceConnection is correctly switched to models.JSONField.


251-257: LGTM!

The input field for Command is correctly switched to models.JSONField.

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (1)

1-47: LGTM!

This test migration correctly mirrors the production migration (0010_replace_jsonfield_with_django_builtin.py). All three AlterField operations match the field definitions in the connection base models with proper encoder=DjangoJSONEncoder. The Ruff RUF012 warnings are false positives for Django migration classes.

tests/openwisp2/sample_config/migrations/0001_initial.py (2)

74-82: LGTM!

All JSONField replacements in this initial migration are correct. The encoder will be applied via the subsequent 0008 migration.


113-124: LGTM!

The remaining JSONField conversions (Config.context, Vpn.config, Template.config, Template.default_values, OrganizationConfigSettings.context, DeviceGroup.meta_data, DeviceGroup.context) are all consistent—load_kwargs/dump_kwargs removed, field attributes preserved.

Also applies to: 238-245, 502-510, 565-577, 693-704, 746-770

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)

1-117: LGTM!

This test migration correctly mirrors the production migration (0062_replace_jsonfield_with_django_builtin.py). All eight AlterField operations are consistent in field attributes and encoder usage. The Ruff RUF012 warnings are false positives for Django migration classes.

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)

1-117: LGTM!

All eight AlterField operations are correct and consistent with the base model definitions. The migration properly depends on the preceding migration (0061_config_checksum_db). Since both the old jsonfield.JSONField and Django's models.JSONField map to the same database column type (e.g., jsonb on PostgreSQL), this migration is safe and should be a no-op at the database level. The Ruff RUF012 warnings are standard false positives for Django migrations.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes OpenWISP Controller models by replacing the unmaintained third-party jsonfield package with Django’s built-in models.JSONField, and adds migrations to alter the affected database columns accordingly.

Changes:

  • Replaced from jsonfield import JSONField usages with Django built-in JSONField in controller model base classes.
  • Added migrations (including test app migrations) to AlterField the affected columns to models.JSONField.
  • Removed jsonfield from requirements.txt and dropped load_kwargs / dump_kwargs parameters.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
requirements.txt Removes the jsonfield dependency.
openwisp_controller/config/base/base.py Switches config field to Django JSONField and removes jsonfield-specific kwargs.
openwisp_controller/config/base/config.py Switches context field to Django JSONField and removes jsonfield-specific kwargs.
openwisp_controller/config/base/device_group.py Switches meta_data / context fields to Django JSONField.
openwisp_controller/config/base/multitenancy.py Switches organization context field to Django JSONField.
openwisp_controller/config/base/template.py Switches default_values to Django JSONField.
openwisp_controller/connection/base/models.py Switches params / input fields to Django JSONField.
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py Alters config-related fields to models.JSONField.
openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py Alters connection-related fields to models.JSONField.
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py Updates sample config app fields to models.JSONField.
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py Updates sample connection app fields to models.JSONField.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to 3
# Generated by Django 5.2.11 on 2026-02-04 02:53

from django.db import migrations, models
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description references a 0061_replace_jsonfield_with_django_builtin.py migration, but this change introduces 0062_replace_jsonfield_with_django_builtin.py (dependency 0061_config_checksum_db). Consider updating the PR description (or renaming/rebasing the migration if needed) to avoid confusion for reviewers and downstream backports.

Copilot uses AI. Check for mistakes.
@coderabbitai coderabbitai bot added the dependencies Pull requests that update a dependency file label Feb 4, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 4, 2026
@Eeshu-Yadav Eeshu-Yadav force-pushed the 1061-replace-thirdparty-jsonfield-with-django-builtin1 branch from cec77ae to 0578eba Compare February 4, 2026 03:18
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)

1-26: ⚠️ Potential issue | 🟡 Minor

Modifying an already-applied migration may cause issues for existing deployments.

This migration (0049_devicegroup_context) may have already been applied in production environments. Modifying its field type from jsonfield.fields.JSONField to models.JSONField won't trigger a re-run of the migration for existing deployments, potentially leaving their schema inconsistent with new deployments.

The dedicated migration 0062_replace_jsonfield_with_django_builtin.py already handles altering this field for existing deployments. Consider whether modifying this historical migration is necessary, or if it would be safer to leave the original migration unchanged and rely solely on the new 0062 migration for the field type conversion.

If the intent is to ensure fresh deployments use Django's JSONField from the start, this approach works but should be documented in upgrade notes.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cec77ae and 0578eba.

📒 Files selected for processing (24)
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • requirements.txt
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
💤 Files with no reviewable changes (1)
  • requirements.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/connection/base/models.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/base/device_group.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/base/multitenancy.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
🧬 Code graph analysis (2)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (7)
openwisp_controller/config/migrations/0018_config_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0028_template_default_values.py (1)
  • Migration (7-25)
openwisp_controller/config/migrations/0036_device_group.py (1)
  • Migration (16-104)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)
  • Migration (7-26)
openwisp_controller/config/base/config.py (1)
  • name (130-137)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (4)
openwisp_controller/config/migrations/0018_config_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
🪛 Ruff (0.14.14)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-110: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-110: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
🔇 Additional comments (24)
openwisp_controller/connection/migrations/0001_initial.py (1)

66-72: LGTM!

The migration correctly uses Django's built-in models.JSONField for Credentials.params and DeviceConnection.params fields. The field attributes (default=dict, help_text, verbose_name, blank) are properly preserved.

Also applies to: 131-141

openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)

4-4: LGTM!

The migration correctly updates to Django's built-in models.JSONField with proper import and field attributes preserved.

Also applies to: 16-24

openwisp_controller/connection/base/models.py (1)

7-7: LGTM!

The model file correctly imports and uses Django's built-in JSONField for all three fields:

  • Credentials.params
  • DeviceConnection.params
  • Command.input

Field attributes are appropriately preserved with default=dict, blank, null, and help_text where applicable.

Also applies to: 99-103, 237-245, 420-423

openwisp_controller/config/migrations/0023_update_context.py (1)

4-4: LGTM!

The migration correctly updates the config.context field to use Django's built-in models.JSONField with all attributes preserved.

Also applies to: 14-22

openwisp_controller/connection/migrations/0007_command.py (1)

75-81: LGTM!

The Command.input field correctly uses Django's built-in models.JSONField with appropriate blank=True and null=True attributes.

openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py (1)

57-65: LGTM!

The squashed migration correctly updates all three config fields to use Django's built-in models.JSONField:

  • Config.config
  • Template.config
  • Vpn.config

All field attributes (default=dict, blank, help_text, verbose_name) are properly preserved.

Also applies to: 247-255, 342-349

openwisp_controller/config/migrations/0028_template_default_values.py (1)

4-4: LGTM!

The migration correctly updates the template.default_values field to use Django's built-in models.JSONField with all attributes preserved.

Also applies to: 14-23

openwisp_controller/config/base/template.py (2)

8-8: LGTM!

The import is correctly updated to use Django's built-in JSONField from django.db.models.


95-105: LGTM!

The default_values field is correctly migrated to Django's built-in JSONField. The removal of load_kwargs and dump_kwargs is appropriate since Django's JSONField handles serialization internally, and Python 3.7+ dicts preserve insertion order, making the OrderedDict hook unnecessary.

openwisp_controller/pki/migrations/0001_initial.py (2)

262-269: Field definition looks correct.

The Cert.extensions field correctly uses models.JSONField with appropriate parameters (blank=True, default=list, help text, and verbose name).


111-118: Migration strategy properly handles both fresh and existing deployments.

The corresponding AlterField migration already exists in migration 0012 (0012_alter_ca_extensions_alter_ca_key_length_and_more.py), which correctly converts the extensions field to models.JSONField for both the Ca and Cert models. This ensures existing deployments that already ran the initial migration can upgrade seamlessly.

openwisp_controller/config/base/multitenancy.py (2)

5-5: LGTM!

The import is correctly updated to use Django's built-in JSONField.


33-40: LGTM!

The context field is correctly migrated to Django's built-in JSONField with appropriate parameters. The removal of load_kwargs/dump_kwargs is correct since Django handles JSON serialization internally.

openwisp_controller/config/base/device_group.py (2)

6-6: LGTM!

The import is correctly updated to use Django's built-in JSONField.


38-55: LGTM!

Both meta_data and context fields are correctly migrated to Django's built-in JSONField with appropriate parameters (blank=True, default=dict, help text, and verbose names).

openwisp_controller/config/migrations/0036_device_group.py (1)

56-67: Field definition is correct; same migration modification consideration applies.

The meta_data field correctly uses models.JSONField with appropriate parameters. As noted for other migration files, ensure the dedicated 0061_replace_jsonfield_with_django_builtin.py migration handles the field type conversion for existing deployments.

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)

1-40: LGTM! This is the correct approach for migrating existing field types.

This dedicated migration properly uses AlterField operations to convert existing jsonfield columns to Django's built-in JSONField for the connection app models. This ensures existing deployments will have their field types updated when running migrations.

The Ruff RUF012 warnings about mutable class attributes (dependencies and operations) are false positives—Django migrations use class attributes by design, and these are not intended to be modified at runtime.

openwisp_controller/config/migrations/0018_config_context.py (1)

14-22: No changes needed — the migration modification is safe and properly handled.

The concern about modifying existing migrations doesn't apply here. The 0018 migration was changed to use Django's built-in JSONField instead of the third-party jsonfield library, which is safe because both implementations store JSON identically at the database level. Users with existing deployments will experience no database inconsistency:

  • Their database already has the JSON column from the original 0018 migration
  • The migration won't re-run (Django tracks applied migrations by name)
  • The follow-up migration 0062_replace_jsonfield_with_django_builtin.py properly uses AlterField operations to update field definitions with default=dict and other properties, which is the correct approach for safe schema refinement

Likely an incorrect or invalid review comment.

tests/openwisp2/sample_pki/migrations/0001_initial.py (1)

128-135: LGTM!

The extensions fields for both Ca and Cert models are correctly migrated to Django's built-in models.JSONField. The field options (blank=True, default=list, help_text, verbose_name) are properly preserved. Since this is an initial migration creating new tables, there are no data migration concerns.

Also applies to: 303-311

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)

1-110: LGTM!

This migration correctly converts all JSONField declarations from the third-party jsonfield package to Django's built-in models.JSONField. The field configurations (blank, default, help_text, verbose_name) are consistent with the existing migrations in the codebase (verified against 0023_update_context.py, 0049_devicegroup_context.py, 0028_template_default_values.py, etc.).

The static analysis warnings about mutable class attributes (RUF012) are false positives—Django migrations conventionally use dependencies and operations as class attributes without type annotations.

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (1)

1-40: LGTM!

This test migration correctly converts the JSONField declarations for Command.input, Credentials.params, and DeviceConnection.params to Django's built-in models.JSONField. The field configurations are appropriate and consistent with the main application's migration patterns.

The RUF012 static analysis warnings are false positives for Django migrations.

tests/openwisp2/sample_connection/migrations/0001_initial.py (1)

69-76: LGTM!

The JSONField declarations for Credentials.params, DeviceConnection.params, and Command.input are correctly migrated to Django's built-in models.JSONField. The field options are properly configured for each use case.

Since this is a test migration under tests/openwisp2/, modifying the initial migration is acceptable as test environments are typically reset more frequently.

Also applies to: 152-163, 251-257

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)

1-110: LGTM!

This test migration correctly mirrors the main application's 0062_replace_jsonfield_with_django_builtin.py migration, ensuring the sample_config test models use Django's built-in models.JSONField. The field configurations are consistent with both the main migration and the relevant code snippets.

The RUF012 static analysis warnings are false positives for Django migrations.

tests/openwisp2/sample_config/migrations/0001_initial.py (1)

74-82: LGTM!

All JSONField declarations across the models (Config.config, Config.context, Vpn.config, Template.config, Template.default_values, OrganizationConfigSettings.context, DeviceGroup.meta_data, DeviceGroup.context) are correctly migrated to Django's built-in models.JSONField with appropriate field options preserved.

Since this is a test migration under tests/openwisp2/, modifying the initial migration is acceptable for ensuring fresh test environments use the correct field types.

Also applies to: 113-124, 238-245, 502-510, 565-577, 693-704, 746-758, 759-770

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 4, 2026
@Eeshu-Yadav Eeshu-Yadav force-pushed the 1061-replace-thirdparty-jsonfield-with-django-builtin1 branch from 0578eba to 8c91a92 Compare February 4, 2026 05:28
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 4, 2026
_("configuration"),
default=dict,
help_text=_("configuration in NetJSON DeviceConfiguration format"),
load_kwargs={"object_pairs_hook": collections.OrderedDict},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add: encoder=DjangoJSONEncoder, see openwisp/openwisp-notifications#438, applies to all JSONField instances

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated that

# 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try removing this after adding encoder=DjangoJSONEncoder

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nemesifier I tested removing the JSON parsing code but it breaks the preview functionality. The DjangoJSONEncoder solves lazy translation serialization but doesn't handle form data parsing. When the admin preview receives form data, JSONField values come as JSON strings that need to be parsed to Python objects before model validation.
Without the parsing code, tests fail with "Unexpected configuration format" because the model expects a dict but receives a string. The encoder and parsing code solve different problems:

  • DjangoJSONEncoder: Handles lazy translations during model save
  • Admin parsing: Converts form strings to Python objects for preview

Both are needed for full functionality. Should I keep the current implementation?

Screenshot from 2026-02-04 23-13-36

@nemesifier nemesifier changed the title [models] Replace thirdparty JSONField with Django built-in JSONField … [change] Replaced thirdparty JSONField with Django built-in JSONField Feb 4, 2026
@coveralls
Copy link

coveralls commented Feb 6, 2026

Coverage Status

coverage: 98.658% (+0.002%) from 98.656%
when pulling 5a81fdf on Eeshu-Yadav:1061-replace-thirdparty-jsonfield-with-django-builtin1
into 2177fea on openwisp:master.

@Eeshu-Yadav Eeshu-Yadav force-pushed the 1061-replace-thirdparty-jsonfield-with-django-builtin1 branch 2 times, most recently from d2f68b5 to 008d787 Compare February 6, 2026 05:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/base/base.py`:
- Around line 125-128: The JSONField declaration for the variable config has
multiple keyword args and the closing parenthesis crammed onto a single overlong
line (JSONField, default=dict, help_text, encoder=DjangoJSONEncoder); split the
arguments across multiple lines so each kwarg (help_text, encoder, etc.) is on
its own line and place the closing parenthesis on its own line to satisfy
line-length rules and restore proper formatting for the JSONField(config,
default=dict, help_text, encoder) call.

In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 34-42: The migration/Model mismatch is caused by
AbstractOrganizationConfigSettings.context, AbstractDeviceGroup.meta_data and
AbstractDeviceGroup.context declaring encoder=DjangoJSONEncoder in the models
but the migration that creates OrganizationConfigSettings and DeviceGroup fields
omits that kwarg; update the migration's field definitions for
OrganizationConfigSettings.context and DeviceGroup.meta_data and
DeviceGroup.context to include encoder=DjangoJSONEncoder (or alternatively
remove encoder from the model fields if you prefer) so the migration's field
kwargs match the model's JSONField.deconstruct() output.

In `@openwisp_controller/config/migrations/0049_devicegroup_context.py`:
- Around line 16-24: The AlterField for the JSONField named "context" must
include the same encoder used in the model: add encoder=DjangoJSONEncoder to the
models.JSONField(...) call in the migration AlterField, and import
DjangoJSONEncoder at the top of the migration (from django.core.serializers.json
import DjangoJSONEncoder) so the migration matches the model's context field
definition in device_group.py.

In `@openwisp_controller/connection/migrations/0001_initial.py`:
- Around line 65-71: The migration JSONField definitions for Credentials.params
and DeviceConnection.params are missing encoder=DjangoJSONEncoder; update both
JSONField calls in openwisp_controller/connection/migrations/0001_initial.py to
include encoder=DjangoJSONEncoder and ensure the migration imports
DjangoJSONEncoder (from django.core.serializers.json import DjangoJSONEncoder)
so the migration matches the model definitions (update the JSONField for the
"params" field in both the Credentials and DeviceConnection model declarations
in the migration).
🧹 Nitpick comments (1)
openwisp_controller/config/base/multitenancy.py (1)

4-6: Minor style note: JSONField can be accessed via the already-imported models module.

Line 5 imports models and line 6 separately imports JSONField from django.db.models. You could use models.JSONField directly (as is done in all the migration files) to avoid the extra import. This is a minor consistency point — the current approach works but differs from the migration convention.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c91a92 and 008d787.

📒 Files selected for processing (27)
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • requirements.txt
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
💤 Files with no reviewable changes (1)
  • requirements.txt
🚧 Files skipped from review as they are similar to previous changes (11)
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/admin.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0036_device_group.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/base/template.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/device_group.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
🧬 Code graph analysis (9)
openwisp_controller/config/base/multitenancy.py (1)
openwisp_controller/config/base/base.py (1)
  • json (274-281)
tests/openwisp2/sample_pki/migrations/0001_initial.py (1)
openwisp_controller/pki/migrations/0012_alter_ca_extensions_alter_ca_key_length_and_more.py (1)
  • Migration (7-70)
openwisp_controller/config/base/config.py (1)
openwisp_controller/config/base/base.py (1)
  • json (274-281)
openwisp_controller/config/base/device_group.py (1)
openwisp_controller/config/base/base.py (1)
  • json (274-281)
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (2)
openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-40)
tests/openwisp2/sample_connection/migrations/0001_initial.py (1)
  • Migration (19-284)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (7)
openwisp_controller/config/migrations/0018_config_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0028_template_default_values.py (1)
  • Migration (7-25)
openwisp_controller/config/migrations/0036_device_group.py (1)
  • Migration (16-104)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)
  • Migration (7-26)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
openwisp_controller/pki/migrations/0001_initial.py (1)
openwisp_controller/pki/migrations/0012_alter_ca_extensions_alter_ca_key_length_and_more.py (1)
  • Migration (7-70)
openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (4)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
openwisp_controller/connection/migrations/0001_initial.py (1)
  • Migration (16-170)
openwisp_controller/connection/migrations/0007_command.py (1)
  • Migration (17-112)
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-40)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (5)
openwisp_controller/config/migrations/0018_config_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
tests/openwisp2/sample_config/migrations/0001_initial.py (1)
  • Migration (22-1039)
🪛 Ruff (0.14.14)
tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-110: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-110: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (12)
openwisp_controller/config/base/config.py (2)

9-11: LGTM — imports updated correctly.

The switch to DjangoJSONEncoder and Django's built-in JSONField is clean. One minor note: since models is already imported on line 10, you could use models.JSONField directly instead of the separate import on line 11, which would be consistent with how other model fields are referenced. But this is purely stylistic and consistent with other files in this PR.


85-95: Context field migration looks correct.

The encoder=DjangoJSONEncoder is a reasonable choice — it handles edge-case types (datetime, Decimal, UUID) that plain json.dumps would reject. The default=dict and blank=True are preserved from the previous declaration.

openwisp_controller/config/base/base.py (1)

8-10: Imports updated consistently with the rest of the PR.

openwisp_controller/config/base/template.py (1)

7-9: LGTM — default_values field correctly migrated to Django's JSONField.

The encoder=DjangoJSONEncoder and removal of load_kwargs/dump_kwargs is consistent with the other model files in this PR. Existing validation logic in clean() (lines 226-231) correctly handles the field as a native Python dict.

Also applies to: 96-107

openwisp_controller/pki/migrations/0001_initial.py (1)

110-118: LGTM — extensions fields migrated to models.JSONField.

Both Ca.extensions and Cert.extensions are correctly updated. No encoder is specified, which is appropriate since these fields store plain lists. The relevant snippet from migration 0012 confirms a later AlterField already covers existing databases with the same models.JSONField signature.

Also applies to: 261-269

openwisp_controller/config/migrations/0018_config_context.py (1)

4-22: In-place modification of historical migration is safe — dedicated transition migration exists.

The changes to 0018_config_context.py are properly handled by migration 0062_replace_jsonfield_with_django_builtin.py, which includes a dedicated AlterField operation for the context field on the config model (lines 23–35). This ensures:

  • Existing databases: The AlterField in 0062 transitions the field with default=dict and removes null=True
  • Fresh installs: Running both migrations in sequence produces the correct final state

Both conditions for safe in-place modification of historical migrations are met.

tests/openwisp2/sample_pki/migrations/0001_initial.py (1)

128-135: LGTM!

Both extensions fields (Ca and Cert) are correctly migrated to models.JSONField with blank=True, default=list, consistent with the production migration in openwisp_controller/pki/migrations/0012_alter_ca_extensions_alter_ca_key_length_and_more.py. Removal of dump_kwargs/load_kwargs is appropriate since Django's built-in JSONField handles serialization internally.

Also applies to: 303-311

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)

1-40: LGTM!

The migration correctly alters all three connection JSON fields to Django's built-in JSONField with appropriate field options. The operations are consistent with the parallel test migration (tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py) and the existing initial migration field definitions. Dependency chain is correct.

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (1)

1-40: LGTM!

Test migration correctly mirrors the production migration (openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py) with identical field definitions for all three connection JSON fields.

tests/openwisp2/sample_connection/migrations/0001_initial.py (1)

69-76: LGTM!

All three JSON fields (Credentials.params, DeviceConnection.params, Command.input) are correctly updated to models.JSONField with field options matching the production initial migration. Clean removal of jsonfield-specific imports and parameters.

Also applies to: 152-163, 251-257

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)

1-110: LGTM!

This test migration correctly mirrors the production migration (openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py) across all 8 AlterField operations. Field definitions, help texts, defaults, and blank/verbose_name settings are consistent. Note that VPN.config correctly omits blank=True (line 104) since VPN configuration is required, unlike the other config fields.

The only pending concern is the encoder=DjangoJSONEncoder mismatch already raised in the multitenancy.py review — if the encoder is kept in the model definitions, it needs to be added here as well.

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)

1-110: Migration looks correct and consistent.

All 8 AlterField operations match the corresponding sample_config test migration (tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py) exactly. The field parameters (blank, default, help_text, verbose_name) are consistent with the earlier migrations that originally defined these fields. The dependency on 0061_config_checksum_db is correct.

The RUF012 static analysis warnings are false positives — dependencies and operations as plain class attributes is the standard Django migration convention.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 65 to 71
(
"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",
),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "JSONField|encoder" openwisp_controller/connection/base/models.py

Repository: openwisp/openwisp-controller

Length of output: 312


Missing encoder=DjangoJSONEncoder in migration — inconsistent with model definitions.

Both Credentials.params and DeviceConnection.params JSONField definitions in the migration omit encoder=DjangoJSONEncoder, but the actual model definitions in connection/base/models.py include this encoder. The migration must be updated to match:

models.JSONField(
    default=dict,
    encoder=DjangoJSONEncoder,  # Add this
    help_text="global connection parameters",
    verbose_name="parameters",
)

This applies to both occurrences (Credentials at lines 65-71 and DeviceConnection at lines 130-141).

🤖 Prompt for AI Agents
In `@openwisp_controller/connection/migrations/0001_initial.py` around lines 65 -
71, The migration JSONField definitions for Credentials.params and
DeviceConnection.params are missing encoder=DjangoJSONEncoder; update both
JSONField calls in openwisp_controller/connection/migrations/0001_initial.py to
include encoder=DjangoJSONEncoder and ensure the migration imports
DjangoJSONEncoder (from django.core.serializers.json import DjangoJSONEncoder)
so the migration matches the model definitions (update the JSONField for the
"params" field in both the Credentials and DeviceConnection model declarations
in the migration).

@Eeshu-Yadav Eeshu-Yadav force-pushed the 1061-replace-thirdparty-jsonfield-with-django-builtin1 branch from 008d787 to 3177b52 Compare February 6, 2026 06:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_controller/connection/tests/test_models.py (1)

601-610: ⚠️ Potential issue | 🟡 Minor

Stale test data: input='["echo test"]' is now stored as a string, not a list.

Line 603 was not updated in this PR. With the old jsonfield, '["echo test"]' was auto-parsed to a Python list ["echo test"]. With Django's JSONField, it will be stored as the literal string '["echo test"]'. While this test still passes (the TypeError is triggered by type="custom", not the input value), the input no longer holds the semantically intended value. Consider updating it for consistency with the rest of the test changes:

-            command = Command(input='["echo test"]', type="custom")
+            command = Command(input=["echo test"], type="custom")
🧹 Nitpick comments (2)
tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py (1)

1-117: Two-step migration in tests vs single-step in main app — consider squashing.

The test app uses two migrations to achieve what the main config app does in one:

  • 0008 converts fields to models.JSONField without encoder
  • 0009 (this file) adds encoder=DjangoJSONEncoder

Meanwhile, openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py does both in a single migration. This means there's an intermediate state (after 0008, before 0009) where the test app's fields lack the encoder that the models specify — Django would detect a pending migration in that window.

Consider squashing 0008 and 0009 into a single migration to mirror the main app's approach and avoid the intermediate inconsistency. That said, this is cosmetic and doesn't affect correctness at runtime once both migrations run.

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py (1)

1-40: Redundant migration — consider squashing into 0005.

This migration alters all three fields to models.JSONField without encoder, and the immediately following 0005 re-alters the exact same fields to add encoder=DjangoJSONEncoder. Since both are new, unapplied migrations introduced in this PR, they can be safely merged into a single migration, avoiding unnecessary ALTER TABLE operations.

The production equivalent (openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py) achieves this in a single migration.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 008d787 and 3177b52.

📒 Files selected for processing (29)
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/migrations/0001_initial.py
  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • requirements.txt
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
💤 Files with no reviewable changes (1)
  • requirements.txt
🚧 Files skipped from review as they are similar to previous changes (11)
  • openwisp_controller/config/base/template.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/config/migrations/0036_device_group.py
  • openwisp_controller/config/migrations/0018_config_context.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py
  • openwisp_controller/config/migrations/0023_update_context.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/migrations/0001_initial.py
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/connection/migrations/0007_command.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/connection/migrations/0007_command.py
  • tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/base/multitenancy.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/config/base/base.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/migrations/0049_devicegroup_context.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:40.078Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:40.078Z
Learning: In test migrations under tests/openwisp2/sample_config/migrations, verify scenarios where a swappable model (CONFIG_WHOISINFO_MODEL) is extended with extra fields (e.g., an additional 'details' field) to ensure compatibility and no errors when swapping to a custom implementation. This pattern helps confirm that extending AbstractWHOISInfo via a custom model works as intended.

Applied to files:

  • tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py
  • tests/openwisp2/sample_config/migrations/0001_initial.py
  • tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/pki/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0028_template_default_values.py
  • tests/openwisp2/sample_connection/migrations/0001_initial.py
  • openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/config/base/multitenancy.py
  • tests/openwisp2/sample_pki/migrations/0001_initial.py
  • tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py
  • tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py
🧬 Code graph analysis (4)
tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py (4)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (8)
openwisp_controller/config/migrations/0018_config_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0023_update_context.py (1)
  • Migration (7-24)
openwisp_controller/config/migrations/0028_template_default_values.py (1)
  • Migration (7-25)
openwisp_controller/config/migrations/0049_devicegroup_context.py (1)
  • Migration (7-26)
openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py (1)
  • Migration (7-26)
openwisp_controller/connection/migrations/0001_initial.py (1)
  • Migration (16-170)
tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)
  • Migration (6-110)
tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
openwisp_controller/config/base/multitenancy.py (1)
openwisp_controller/config/base/base.py (1)
  • json (276-283)
tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py (3)
openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-47)
tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py (1)
  • Migration (7-117)
🪛 Ruff (0.14.14)
tests/openwisp2/sample_config/migrations/0009_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-117: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-117: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-110: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py

[warning] 9-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 13-47: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py

[warning] 8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 12-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🔇 Additional comments (20)
openwisp_controller/connection/tests/test_models.py (3)

503-511: LGTM — test correctly updated for native Python objects.

With Django's built-in JSONField, the field accepts native Python objects directly. Passing ["test"] instead of a JSON string '["test"]' is the correct approach, and the updated assertion matches the new validation behavior.


529-548: LGTM — validation error messages updated correctly.

With Django's JSONField, the "notjson" string is stored as-is (a Python string), so the JSON schema validator now sees a string type rather than a parse error. The updated error messages ("'notjson' is not of type 'object'" and "[] is not of type 'object'") correctly reflect the new behavior.


973-1003: LGTM — config values correctly changed from JSON strings to Python dicts.

openwisp_controller/pki/migrations/0001_initial.py (2)

110-118: LGTM — Ca.extensions field correctly migrated to Django's JSONField.

The field definition matches the later 0012 migration's target state (including help_text), ensuring consistency for both fresh installs and upgrades.


261-269: LGTM — Cert.extensions field correctly migrated.

Consistent with Ca.extensions and the 0012 migration.

openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py (3)

57-65: LGTM — Config.config field correctly updated in initial migration.

The encoder is intentionally absent here since it will be added by the later 0062 migration.


247-255: LGTM — Template.config field correctly updated.


342-349: LGTM — Vpn.config field correctly updated.

Correctly preserves the distinction that Vpn.config does not have blank=True, unlike Config.config and Template.config.

openwisp_controller/config/base/multitenancy.py (1)

4-6: LGTM — imports updated correctly.

Clean switch from jsonfield to Django's built-in JSONField with DjangoJSONEncoder.

openwisp_controller/config/base/base.py (1)

8-10: LGTM — imports cleanly updated.

Correctly replaces jsonfield imports with Django's built-in JSONField and adds DjangoJSONEncoder.

openwisp_controller/connection/migrations/0007_command.py (1)

75-81: The migration mismatch has been resolved in a subsequent migration. Migration 0010_replace_jsonfield_with_django_builtin.py (applied after 0007_command.py) already added encoder=DjangoJSONEncoder to the input field via AlterField, bringing it in sync with the model definition in base/models.py.

tests/openwisp2/sample_connection/migrations/0005_replace_jsonfield_with_django_builtin.py (1)

1-47: LGTM!

All three fields include encoder=DjangoJSONEncoder, matching the production migration (0010). Field definitions (defaults, help_text, verbose_name) are consistent.

openwisp_controller/config/migrations/0028_template_default_values.py (1)

4-24: LGTM!

Clean replacement of jsonfield.fields.JSONField with models.JSONField. The encoder is correctly deferred to the later 0062 migration. Field attributes (blank, default, help_text, verbose_name) are preserved.

openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py (1)

1-47: LGTM!

Single, clean migration with encoder=DjangoJSONEncoder on all three connection fields. Dependency chain is correct, and field attributes match the base model definitions.

tests/openwisp2/sample_connection/migrations/0001_initial.py (1)

71-75: LGTM!

All three JSONField replacements in the initial migration are correct. The encoder is appropriately deferred to the later dedicated migration (0004/0005). Field attributes are preserved.

Also applies to: 154-162, 253-256

tests/openwisp2/sample_config/migrations/0001_initial.py (1)

76-81: LGTM!

All eight JSONField replacements in the initial migration are correct. Field attributes are preserved, and the encoder is appropriately handled by the later dedicated migrations.

Also applies to: 115-123, 240-244, 504-509, 567-576, 695-703, 748-757, 761-769

tests/openwisp2/sample_config/migrations/0008_replace_jsonfield_with_django_builtin.py (1)

12-109: No action needed — migration chain produces correct final state.

Migration 0008 defers the encoder parameter to follow-up migration 0009, which adds encoder=django.core.serializers.json.DjangoJSONEncoder to all 8 fields (not just 2 as claimed). The final schema matches production migration 0062 exactly, with all fields properly configured to serialize datetimes, Decimals, UUIDs, and lazy translation objects.

tests/openwisp2/sample_pki/migrations/0001_initial.py (2)

1-10: Removed imports and field parameter cleanup look correct.

The jsonfield.fields and collections imports have been properly removed, and load_kwargs/dump_kwargs parameters are no longer present. The migration header still says "Generated by Django 3.0.6" — this is expected since only field definitions were edited in-place, not the migration itself regenerated.


128-134: Consider adding encoder=DjangoJSONEncoder to the extensions field or evaluate if it's necessary for this specific field type.

The review comment's claim about encoder consistency is inaccurate: sample_config/0008 (also in this PR) similarly lacks the encoder—it was added only in the follow-up migration 0009. Additionally, the production migration pki/0012_alter_ca_extensions_... for the actual Ca and Cert models also omits the encoder on the extensions fields.

If DjangoJSONEncoder is needed to handle non-JSON-native types (UUID, Decimal, datetime) that may appear in X.509 extensions, it should be added here. Otherwise, if extensions data are always JSON-serializable lists/dicts (as the default=list suggests), the current approach aligns with the production code and initial config approach (before its follow-up encoder migration).

openwisp_controller/config/migrations/0062_replace_jsonfield_with_django_builtin.py (1)

1-117: Migration looks correct and well-structured.

All eight AlterField operations consistently apply models.JSONField with encoder=DjangoJSONEncoder, and the field attributes (defaults, help texts, verbose names, blank) align with both the existing migration history and the parallel test migration in sample_config. The dependency on 0061_config_checksum_db is correct.

The RUF012 static analysis warnings about dependencies and operations are false positives — these are standard Django migration class attributes that follow the framework's conventions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

…penwisp#1061

Replaced jsonfield package's JSONField with Django's built-in JSONField
across all models to use the modern, maintained Django implementation.

Changes:
- Updated imports from 'jsonfield import JSONField' to 'django.db.models import JSONField'
- Removed jsonfield-specific parameters (load_kwargs, dump_kwargs) which are not needed
- Created migrations to alter field types while preserving data
- Removed unnecessary collections imports where no longer needed

Django's JSONField preserves the same data format and is backward compatible,
so no data migration is required. The built-in JSONField provides better
performance and is actively maintained as part of Django core.

Fixes openwisp#1061
@Eeshu-Yadav Eeshu-Yadav force-pushed the 1061-replace-thirdparty-jsonfield-with-django-builtin1 branch from 3177b52 to 5a81fdf Compare February 6, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[change:controller] Replace thirdparty JSONField with Django built in JSONField

3 participants