In Django's JSONField, how do SQL NULL and JSON null differ, and why must its default be a callable like dict?
answer
- None at the top level
- a 6.1 expression for JSON null
- both read back as None
- isnull versus exact match
- one shared mutable object
basics
~20 sAssigning None to a JSONField stores SQL NULL; a JSON null needs Django 6.1's JSONNull() expression, and both read back as None. Its default must be a callable such as dict, or all instances share one mutable object.
solid answer
~40 sA `JSONField` holds Python dicts, lists, strings, numbers, booleans and `None`. At the top level, assigning `None` stores **SQL `NULL`** (which needs `null=True`), while Django 6.1's `JSONNull()` stores the **JSON scalar `null`** — and a JSON `null` does not violate `null=False`. Both come back as `None`, so they are hard to tell apart; query them with `data__isnull=True` for SQL `NULL` and `data=JSONNull()` for JSON `null`. Using `None` in an exact filter to mean JSON `null` is deprecated in 6.1 and will later mean SQL `NULL`. For defaults, `default={}` or `default=[]` shares one mutable object across all instances, and a system check warns about it (`fields.E010`); use `default=dict` or `default=list`. Django's documentation recommends `null=False` with such a default unless you need SQL `NULL`.
code
python · 7 linesfrom django.db.models import JSONNull
MenuItem.objects.create(name="Pho", kitchen_notes=None) # SQL NULL
MenuItem.objects.create(name="Laksa", kitchen_notes=JSONNull()) # JSON null
MenuItem.objects.filter(kitchen_notes__isnull=True) # Pho
MenuItem.objects.filter(kitchen_notes=JSONNull()) # Laksago deeper
Use default=dict or default=list on a JSONField and know that assigning None stores SQL NULL.
Explain SQL NULL versus JSON null, JSONNull() in 6.1, how to query each, and why a mutable default is shared.
Remove ambiguous filter(data=None) calls before 7.0, prefer null=False with a callable default, and move frequently filtered keys into real columns.
Set rules for when semi-structured JSON is acceptable in the schema versus modelled columns, including how nulls and missing keys are interpreted.
## What a JSONField stores `models.JSONField` stores JSON-encodable Python data: dictionaries, lists, strings, numbers, booleans and `None`. It works on MariaDB, MySQL, Oracle, PostgreSQL (as `jsonb`) and SQLite with the JSON1 extension. On a menu item it suits loosely structured data such as allergen notes or kitchen tags: ```python from django.db import models class MenuItem(models.Model): name = models.CharField(max_length=120) allergens = models.JSONField(default=list, blank=True) kitchen_notes = models.JSONField(null=True, blank=True) ``` ## Two kinds of null A JSON column can be empty in two different ways: | stored as | how to write it | how to query it | allowed with `null=False`? | |---|---|---|---| | SQL `NULL` | assign `None` | `kitchen_notes__isnull=True` | no | | JSON scalar `null` | assign `JSONNull()` (Django 6.1) | `kitchen_notes=JSONNull()` | yes | Key facts: 1. Assigning `None` as the **top-level** value stores SQL `NULL`, like any other field. 2. To store the JSON literal `null` instead, Django 6.1 adds the **`JSONNull()`** expression from `django.db.models`. Earlier releases used `Value(None, JSONField())` for the same job. 3. **Both read back as Python `None`**, so once saved you cannot tell them apart from the attribute alone — only a query distinguishes them. 4. `None` **inside** a dict or list, as in `{"nuts": None}`, is always JSON `null`; the distinction applies only to the top-level value. 5. Storing JSON `null` does **not** violate `null=False`, because the column holds a non-NULL JSON value. ## The 6.1 deprecation Before Django 6.1, `filter(kitchen_notes=None)` matched **JSON `null`**, which surprised people who expected SQL `NULL`. Django 6.1 deprecates that meaning: the filter now emits a `RemovedInDjango70Warning`, and after the deprecation period `None` will compile to SQL `IS NULL`. Write the intent explicitly: - `filter(kitchen_notes__isnull=True)` for SQL `NULL`; - `filter(kitchen_notes=JSONNull())` for JSON `null`. Key and index lookups such as `allergens__0` or `kitchen_notes__station` are unaffected by this change. ## Why the default must be a callable A field's `default` is evaluated once, when the model class is defined, unless it is callable. With `default={}` or `default=[]`, every new `MenuItem` would receive a reference to the **same** object; appending an allergen to one unsaved item would change the default for the next. So: - use `default=dict` or `default=list`, or a function returning a fresh object; - a lambda does not work for field options, because migrations cannot serialize it — use a module-level function; - `JSONField` runs a system check that **warns** (`fields.E010`) when its default is a non-callable, non-`None` value. Django's documentation recommends `null=False` plus a default such as `dict` unless you really need to distinguish SQL `NULL`, which avoids the two-nulls question entirely. ## Validating the shape The database checks only that the value is valid JSON; it does not know that `allergens` should be a list of strings. Add a validator for the shape you expect — it runs in forms and `full_clean()`: ```python from django.core.exceptions import ValidationError def validate_allergen_list(value): if not isinstance(value, list) or not all(isinstance(v, str) for v in value): raise ValidationError("Allergens must be a list of names.") ``` Then declare `models.JSONField(default=list, blank=True, validators=[validate_allergen_list])`. As with every validator, `save()`, `update()` and `bulk_create()` do not run it. ## Other practical points - **Encoders** — `encoder=DjangoJSONEncoder` lets you save `datetime`, `Decimal` or `UUID` values, but they come back as strings unless a custom `decoder` converts them. - **Indexes** — `db_index=True` builds a B-tree index that rarely helps JSON queries; PostgreSQL's GIN indexes are the usual tool there. - **Validation** — `choices`, `blank` and validators apply as for other fields; the database checks only that the value is valid JSON. - **Schema discipline** — a JSONField is flexible, but anything you filter or sort on routinely usually deserves its own column.
- What changes in Django 6.1 for filter(data=None) on a JSONField?It still matches JSON `null` but emits a `RemovedInDjango70Warning`; after the deprecation period it will mean SQL `NULL`. Use `data__isnull=True` for SQL `NULL` and `data=JSONNull()` for JSON `null` to state the intent explicitly.
- Why can't default=lambda: {} be used on a JSONField?Migrations must serialize field options, and a lambda cannot be serialized. Use `default=dict` or a named module-level function that returns a new object.
saying these in an interview costs you the question
- assigning None to a JSONField stores the JSON literal null
- SQL NULL and JSON null read back as different Python values
- default={} gives every instance its own empty dict
- storing JSON null violates null=False
- filter(data=None) matches SQL NULL rows in Django 6.1