When a model needs a few optional fields — a UI theme, a language, notification toggles — the cleanest option is not a new column for each option, but Django’s JSONField: a single column that stores a dict, can still be queried through the ORM, and accepts new keys without a migration. This fits flexible data that is rarely filtered in complex ways and needs no strict database constraints; for important data that needs joins or constraints, use dedicated columns.
The problem
Imagine a user profile table (UserProfile) that keeps growing: first theme, then language, then notifications_email, then notifications_sms. Every new option means a new column, a new migration, and a model that gets harder to read. The common workaround is no better: storing a JSON string in a TextField and calling json.loads() on every read. That way you cannot filter directly in the database — to find users who chose the dark theme, you have to pull every record into Python and filter by hand.
The solution
Declare a JSONField with an empty dict as its default:
from django.db import modelsclass UserProfile(models.Model): user = models.OneToOneField("auth.User", on_delete=models.CASCADE) preferences = models.JSONField(default=dict, blank=True)
Note: use default=dict, not default={}. A single shared dict would make a change on one record leak into every other record.
Creating and querying both read like ordinary Python code:
profile = UserProfile.objects.create( user=user, preferences={ "theme": "dark", "language": "vi", "notifications": {"email": True, "sms": False}, },)# Filter by a top-level keydark_users = UserProfile.objects.filter(preferences__theme="dark")# Filter by a nested key, using __ to go deeperemail_on = UserProfile.objects.filter(preferences__notifications__email=True)# Only records that have a given keyhas_theme = UserProfile.objects.filter(preferences__has_key="theme")
To change one key without overwriting the whole dict, copy it, assign it back, then save just that column:
prefs = dict(profile.preferences)prefs["theme"] = "light"profile.preferences = prefsprofile.save(update_fields=["preferences"])
Do not edit the dict in place, call save(), and hope Django notices the change — the copy-and-reassign approach above always works. If you filter on JSON often on PostgreSQL, add a GinIndex on that column so queries do not scan the whole table.
Apply it to your project
JSONField fits “soft” data: UI preferences, notification settings, product metadata, or per-tenant configuration in a multi-tenant app. The practical rule: if a key starts showing up in every important query, needs a uniqueness constraint, or needs to join with another table, split it into its own column. Everything else can stay in the JSONField and keep things tidy.
Related posts
- Django: Fix N+1 Queries with select_related in 5 Minutes
- Django: Lighter Querysets with values() and values_list() in 5 Minutes
- Django: Group Stats with values() + annotate() — GROUP BY in a Single Query
Frequently asked questions
What data types can JSONField store?
Dicts, lists, strings, numbers, True/False and None — everything JSON can represent. It cannot store datetimes or arbitrary Python objects directly; convert those to strings or numbers first.
How is JSONField different from a TextField holding a JSON string?
A TextField only stores a string, so the database does not understand the structure inside and cannot filter by key. JSONField is real JSON as far as the database is concerned, so you can filter, check that a key exists, and (on PostgreSQL) index it.
Does adding a new key need a migration?
No. Adding, renaming or removing a key inside the dict does not change the schema, so no migration is needed. You only need a migration when you add, change or remove the JSONField column itself, or change its index.
When should I not use JSONField?
When the data needs foreign keys, uniqueness constraints, strict NOT NULL checks, or complex aggregation on that very field. Those cases deserve their own columns with clear types.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.
Sources
- Django documentation — Model field reference: JSONField: https://docs.djangoproject.com/en/stable/ref/models/fields/#jsonfield
- Django documentation — Querying JSONField: https://docs.djangoproject.com/en/stable/topics/db/queries/#querying-jsonfield
