summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClifford Gama <cliffygamy@gmail.com>2025-10-24 23:38:52 +0200
committerJacob Walls <jacobtylerwalls@gmail.com>2025-10-29 15:00:52 -0400
commit348ca845385beaddc7c862ff8ec369f041a5088d (patch)
treed8772ba51267934216ecb0c2de0786b4702a3310
parentbe7f68422d4c6ae568a17f1fa91aac67d284df82 (diff)
Refs #35381 -- Deprecated using None in JSONExact rhs to mean JSON null.
Key and index lookups are exempt from the deprecation. Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
-rw-r--r--django/db/models/fields/json.py20
-rw-r--r--docs/releases/6.1.txt7
-rw-r--r--docs/topics/db/queries.txt14
-rw-r--r--tests/model_fields/test_jsonfield.py47
-rw-r--r--tests/postgres_tests/test_array.py12
5 files changed, 99 insertions, 1 deletions
diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py
index 16be6846ff..819c87119a 100644
--- a/django/db/models/fields/json.py
+++ b/django/db/models/fields/json.py
@@ -1,4 +1,5 @@
import json
+import warnings
from django import forms
from django.core import checks, exceptions
@@ -11,6 +12,7 @@ from django.db.models.lookups import (
PostgresOperatorLookup,
Transform,
)
+from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
from django.utils.translation import gettext_lazy as _
from . import Field
@@ -332,10 +334,24 @@ class CaseInsensitiveMixin:
class JSONExact(lookups.Exact):
+ # RemovedInDjango70Warning: When the deprecation period is over, remove
+ # the following line.
can_use_none_as_rhs = True
def process_rhs(self, compiler, connection):
+ if self.rhs is None and not isinstance(self.lhs, KeyTransform):
+ warnings.warn(
+ "Using None as the right-hand side of an exact lookup on JSONField to "
+ "mean JSON scalar 'null' is deprecated. Use JSONNull() instead (or use "
+ "the __isnull lookup if you meant SQL NULL).",
+ RemovedInDjango70Warning,
+ skip_file_prefixes=django_file_prefixes(),
+ )
+
rhs, rhs_params = super().process_rhs(compiler, connection)
+
+ # RemovedInDjango70Warning: When the deprecation period is over, remove
+ # The following if-block entirely.
# Treat None lookup values as null.
if rhs == "%s" and (*rhs_params,) == (None,):
rhs_params = ("null",)
@@ -547,6 +563,10 @@ class KeyTransformIn(lookups.In):
class KeyTransformExact(JSONExact):
+ # RemovedInDjango70Warning: When deprecation period ends, uncomment the
+ # flag below.
+ # can_use_none_as_rhs = True
+
def process_rhs(self, compiler, connection):
if isinstance(self.rhs, KeyTransform):
return super(lookups.Exact, self).process_rhs(compiler, connection)
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 412ec692e3..dba26cca05 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -360,6 +360,13 @@ Miscellaneous
is deprecated. Pass an explicit field name, like
``values_list("pk", flat=True)``.
+* The use of ``None`` to represent a top-level JSON scalar ``null`` when
+ querying :class:`~django.db.models.JSONField` is now deprecated in favor of
+ the new :class:`~django.db.models.JSONNull` expression. At the end
+ of the deprecation period, ``None`` values compile to SQL ``IS NULL`` when
+ used as the top-level value. :lookup:`Key and index lookups <jsonfield.key>`
+ are unaffected by this deprecation.
+
Features removed in 6.1
=======================
diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt
index 788a418e4f..b3b6ec125d 100644
--- a/docs/topics/db/queries.txt
+++ b/docs/topics/db/queries.txt
@@ -1069,6 +1069,11 @@ as JSON ``null``.
When querying, :lookup:`isnull=True <isnull>` is used to match SQL ``NULL``,
while exact-matching ``JSONNull()`` is used to match JSON ``null``.
+.. deprecated:: 6.1
+
+ Exact-matching ``None`` in a query to mean JSON ``null`` is deprecated.
+ After the deprecation period, it will be interpreted as SQL ``NULL``.
+
.. versionchanged:: 6.1
``JSONNull()`` expression was added.
@@ -1080,6 +1085,12 @@ while exact-matching ``JSONNull()`` is used to match JSON ``null``.
<Dog: Max>
>>> Dog.objects.create(name="Archie", data=JSONNull()) # JSON null.
<Dog: Archie>
+ >>> Dog.objects.filter(data=None)
+ ...: RemovedInDjango70Warning: Using None as the right-hand side of an
+ exact lookup on JSONField to mean JSON scalar 'null' is deprecated. Use
+ JSONNull() instead (or use the __isnull lookup if you meant SQL NULL).
+ ...
+ <QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=JSONNull())
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
@@ -1087,6 +1098,9 @@ while exact-matching ``JSONNull()`` is used to match JSON ``null``.
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>
+.. RemovedInDjango70Warning: Alter the example with the deprecation warning to:
+ <QuerySet [<Dog: Max>]>.
+
Unless you are sure you wish to work with SQL ``NULL`` values, consider setting
``null=False`` and providing a suitable default for empty values, such as
``default=dict``.
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index fd2a880f99..937b557794 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -41,8 +41,15 @@ from django.db.models.fields.json import (
KeyTransformTextLookupMixin,
)
from django.db.models.functions import Cast
-from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
+from django.test import (
+ SimpleTestCase,
+ TestCase,
+ ignore_warnings,
+ skipIfDBFeature,
+ skipUnlessDBFeature,
+)
from django.test.utils import CaptureQueriesContext
+from django.utils.deprecation import RemovedInDjango70Warning
from .models import (
CustomJSONDecoder,
@@ -229,6 +236,8 @@ class TestSaveLoad(TestCase):
self.assertIsNone(obj.value)
@skipUnlessDBFeature("supports_primitives_in_json_field")
+ # RemovedInDjango70Warning.
+ @ignore_warnings(category=RemovedInDjango70Warning)
def test_json_null_different_from_sql_null(self):
json_null = NullableJSONModel.objects.create(value=Value(None, JSONField()))
NullableJSONModel.objects.update(value=Value(None, JSONField()))
@@ -242,6 +251,9 @@ class TestSaveLoad(TestCase):
)
self.assertSequenceEqual(
NullableJSONModel.objects.filter(value=None),
+ # RemovedInDjango70Warning: When the deprecation ends, replace
+ # with:
+ # [sql_null],
[json_null],
)
self.assertSequenceEqual(
@@ -1365,3 +1377,36 @@ class JSONNullTests(TestCase):
obj.refresh_from_db()
self.assertIsNone(obj.value["name"])
self.assertEqual(obj.value["array"], [1, None])
+
+
+# RemovedInDjango70Warning.
+@skipUnlessDBFeature("supports_primitives_in_json_field")
+class JSONExactNoneDeprecationTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.msg = (
+ "Using None as the right-hand side of an exact lookup on JSONField to mean "
+ "JSON scalar 'null' is deprecated. Use JSONNull() instead (or use the "
+ "__isnull lookup if you meant SQL NULL)."
+ )
+ cls.obj = NullableJSONModel.objects.create(value=JSONNull())
+
+ def test_filter(self):
+ with self.assertWarnsMessage(RemovedInDjango70Warning, self.msg):
+ self.assertSequenceEqual(
+ NullableJSONModel.objects.filter(value=None), [self.obj]
+ )
+
+ def test_annotation_q_filter(self):
+ qs = NullableJSONModel.objects.annotate(
+ has_empty_data=Q(value__isnull=True) | Q(value=None)
+ ).filter(has_empty_data=True)
+ with self.assertWarnsMessage(RemovedInDjango70Warning, self.msg):
+ self.assertSequenceEqual(qs, [self.obj])
+
+ def test_case_when(self):
+ qs = NullableJSONModel.objects.annotate(
+ has_json_null=Case(When(value=None, then=Value(True)), default=Value(False))
+ ).filter(has_json_null=True)
+ with self.assertWarnsMessage(RemovedInDjango70Warning, self.msg):
+ self.assertSequenceEqual(qs, [self.obj])
diff --git a/tests/postgres_tests/test_array.py b/tests/postgres_tests/test_array.py
index e65009ad83..f35211e8ed 100644
--- a/tests/postgres_tests/test_array.py
+++ b/tests/postgres_tests/test_array.py
@@ -16,6 +16,7 @@ from django.db.models.functions import Cast, JSONObject, Upper
from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature
from django.test.utils import isolate_apps
from django.utils import timezone
+from django.utils.deprecation import RemovedInDjango70Warning
from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase, PostgreSQLWidgetTestCase
from .models import (
@@ -1586,6 +1587,17 @@ class TestJSONFieldQuerying(PostgreSQLTestCase):
self.assertSequenceEqual(
OtherTypesArrayModel.objects.filter(json__1__isnull=True), [obj]
)
+ # RemovedInDjango70Warning.
+ msg = (
+ "Using None as the right-hand side of an exact lookup on JSONField to mean "
+ "JSON scalar 'null' is deprecated. Use JSONNull() instead (or use the "
+ "__isnull lookup if you meant SQL NULL)."
+ )
+ with self.assertWarnsMessage(RemovedInDjango70Warning, msg):
+ # RemovedInDjango70Warning: deindent, and replace [] with [obj].
+ self.assertSequenceEqual(
+ OtherTypesArrayModel.objects.filter(json__1=None), []
+ )
def test_saving_and_querying_for_json_null(self):
obj = OtherTypesArrayModel.objects.create(json=[JSONNull(), JSONNull()])