Opened 19 months ago
Last modified 4 days ago
#35381 closed New feature
Provide `JSONNull` expression to represent JSON `null` value — at Version 14
| Reported by: | Olivier Tabone | Owned by: | |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | David Sanders, Simon Charette, Mariusz Felisiak, David Wobrock, Sage Abdullah, Clifford Gama, Adam Johnson | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description (last modified by )
From Simon Charette in comment:3, in reference to the issues between SQL's NULLL and JSON's null:
In order to finally solve this null ambiguity problem we should:
- Introduce a
JSONNullexpression to disambiguate between the two. It would also allow the creation of model instances with a JSONnullwhich is convoluted today as it requiresmodels.Value(None, models.JSONField())to be used. - Deprecate
filter(jsonfield=None)meaning JSONnullby requiringJSONNullto be used instead. Should we only do this at the top level to still allowjsonfield__key=Noneto filter against null keys? An alternative would be to introduce a__jsonnulllookup.
The good news is that Django makes it very hard to store JSON null in the first place since #34754 so this kind of constraints should be rarely needed.
Old description
Regression is still present in 5.0 and main branch
given a model defined as follow
from django.db import models
class JSONFieldModel(models.Model):
data = models.JSONField(null=True, blank=True, default=None)
class Meta:
required_db_features = {"supports_json_field"}
constraints = [
models.CheckConstraint(
check=~models.Q(data=models.Value(None, models.JSONField())),
name="json_data_cant_be_json_null",
),
]
the following test works in django 4.1.13, fails in later version
from django.test import TestCase, skipUnlessDBFeature
from .models import JSONFieldModel
class CheckConstraintTests(TestCase):
@skipUnlessDBFeature("supports_json_field")
def test_full_clean_on_null_value(self):
instance = JSONFieldModel.objects.create(data=None) # data = SQL Null value
instance.full_clean()
tests/runtests.py json_null_tests
======================================================================
ERROR: test_full_clean_on_null_value (json_null_tests.tests.CheckConstraintTests.test_full_clean_on_null_value)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/olivier/dev/django.dev/olibook/django/django/test/testcases.py", line 1428, in skip_wrapper
return test_func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/olivier/dev/django.dev/olibook/django/tests/json_null_tests/tests.py", line 13, in test_full_clean_on_null_value
instance.full_clean()
File "/Users/olivier/dev/django.dev/olibook/django/django/db/models/base.py", line 1504, in full_clean
raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Constraint “json_data_cant_be_json_null” is violated.']}
----------------------------------------------------------------------
Attached a patch that will create run json_null_tests folder in django's tests directories. Test can be run with
tests/runtests.py json_null_tests
NOTE : in django 5.1 the CheckConstraint.check parameter as been renamed to CheckConstraint.condition.
I ran a git bisect on this test case:
8fcb9f1f106cf60d953d88aeaa412cc625c60029 is bad
e14d08cd894e9d91cb5d9f44ba7532c1a223f458 is good
git bisect run tests/runtests.py json_null_tests
5c23d9f0c32f166c81ecb6f3f01d5077a6084318 is identified as first bad commit
the issue appears with both sqlite and postgres backends
A few words on what we I'm trying to achieve (some context on the regression):
The json_data_cant_be_json_null aims to prevent having JsonModel.data = None (SQL Null) and JSONModel.data = Value(None, JSONField()) (JSON null) values in the database. These leads to unwanted behaviors when filtering on the null value, and I settled on representing the absence of value with SQL Null and not JSON null.
The app was running in django 4.1 and I'm upgrading it to later django version. That's how the issue appeared.
Change History (15)
by , 19 months ago
| Attachment: | json_null_tests.patch added |
|---|
comment:1 by , 19 months ago
| Cc: | added |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
| Type: | Uncategorized → Bug |
comment:3 by , 18 months ago
I've looked into it as well and #35167 only provides a partial solution here IMO as it would require the Q.check logic to also pass around for_save to activate the code path it introduces.
I'm starting to believe that in order to finally solve this null ambiguity problem we should
- Introduce a
JSONNullexpression to disambigutate between the two. It would also allow the creation of model instances with a JSONnullwhich is convoluated today as it requiresmodels.Value(None, models.JSONField())to be used. - Deprecate
filter(jsonfield=None)meaning JSONnullby requiringJSONNullto be used instead. Should we only do this at the top level to still allowjsonfield__key=Noneto filter againstnullkeys? An alternative would be to introduce a__jsonnulllookup.
The good news is that Django makes it very hard to store JSON null in the first place since #34754 so this kind of constraints should be rarely needed.
comment:4 by , 18 months ago
Thank you Simon. In my view, and in an ideal situation, we shouldn't allow "anywhere" to use None as the json's null. I think we should reserve None only to represent SQL's NULL, and have a representation for the (string) json's null (I understand this matches the new JSONNull expression you are proposing).
Should we only do this at the top level to still allow
jsonfield__key=Noneto filter against null keys?
I would say no, because it then gets very confusing.
An alternative would be to introduce a
__jsonnulllookup.
Would this lookup allow further lookups to be chained?
comment:5 by , 18 months ago
I think we should reserve None only to represent SQL's NULL, and have a representation for the (string) json's null (I understand this matches the new JSONNull expression you are proposing).
Yes it matches the JSONNull proposal. If we take away the ability to use None for filtering against JSON null we should provide an alternative.
The deprecation could look like
- Introduce
JSONNull - Make
filter(jsonfield=None)raise a deprecation warning pointing at either usingfilter(jsonfield__isnull=True)orfilter(jsonfield=JSONNull) - At the end of the deprecation period switch
filter(jsonfield=None)to meanfilter(jsonfield__isnull=True)like on all other fields
It leaves the problem of having JSON null not surviving a round trip to the database as both SQL NULL and json.loads("null") are turned into Python None but that's a different issue that can be addressed with a specialized decoder if users require it.
Would this lookup allow further lookups to be chained?
It could or not, what gets returned from a lookup can be prevent further chaining if we want.
follow-up: 7 comment:6 by , 15 months ago
Hello, if you're okay with it, I would like to start working on this ticket and submit a PR to address the issue.
comment:7 by , 15 months ago
| Cc: | added |
|---|
Replying to Mohammad Salehi:
Hello, if you're okay with it, I would like to start working on this ticket and submit a PR to address the issue.
Of course! Feel free to assign the ticket to you and open a patch.
Bear in mind the suggested approach from the discussions above :)
comment:8 by , 14 months ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:9 by , 6 months ago
| Owner: | changed from to |
|---|
comment:10 by , 5 months ago
| Cc: | added |
|---|
In the context of investigating #36418, I have re-reviewed the different tickets that we have related to confusing behaviours of SQL's NULL vs JSON's string null, and I think this ticket should be edited in summary and description to match the New Feature described in comment:5 (or alternatively, close this one as invalid and open a new ticket).
comment:11 by , 5 months ago
| Cc: | added |
|---|
comment:12 by , 5 months ago
| Cc: | added |
|---|
comment:13 by , 5 months ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:14 by , 4 months ago
| Description: | modified (diff) |
|---|---|
| Summary: | Regression on json null value constraints in django 4.2 → Provide `JSONNull` expression to represent JSON `null` value |
| Type: | Bug → New feature |
| Version: | 5.0 → dev |
I think this is related to #34754, and I originally thought that this was another consequence of what Simon said there: I think the fundamental problem here is that:
but then I ran the proposed test with
--debug-sql(and postgres) and noticed that the check code is casting the value in the database to::JSONBindependently of whether the value isNoneor not:INSERT INTO "ticket_35381_jsonfieldmodel" ("data") VALUES (NULL) RETURNING "ticket_35381_jsonfieldmodel"."id"; args=(NONE,); ALIAS=DEFAULT (0.000) SELECT 1 AS "_check" WHERE COALESCE((NOT ('null'::JSONB = 'null'::JSONB)), TRUE); args=(Int4(1), Jsonb(NONE), Jsonb(NONE), TRUE); ALIAS=DEFAULTSo I kept digging and bisected the "bad" commit to 5c23d9f0c32f166c81ecb6f3f01d5077a6084318, and something like this makes the test pass:
django/db/models/fields/json.py
hasattr(value, "as_sql"):it makes the test
model_fields.test_jsonfield.TestSaveLoad.test_json_null_different_from_sql_nullfails for the check on the query:So further investigation is needed to come up with an acceptable solution.