Typed Django model fields in one JSON column
Django JSONStore keeps model field values in one JSON column. You declare typed
fields on the model as usual. Each field reads and writes a key inside a single
JSONField. Forms, the admin, queries and ordering use them in the
same way as concrete fields.
import jsonstore class Employee(models.Model): data = models.JSONField(default=dict) full_name = jsonstore.CharField(max_length=250) hire_date = jsonstore.DateField() salary = jsonstore.DecimalField(max_digits=10, decimal_places=2) # One column holds all three values. makemigrations produces nothing. employee = Employee(full_name="Ann Lee", salary=Decimal("100.00")) employee.data # {"full_name": "Ann Lee", "salary": "100.00"}
What changes
A new field becomes a code change, not a migration.
The model needs one models.JSONField, and that column takes one
migration. Every virtual field after it lives in the document. There is no
ALTER TABLE, no lock on a large table, and no migration to review.
A tenant, a form builder or a settings model can hold a different set of
fields for each subclass, in one table.
The tradeoff is at the database level: these fields have no constraint and no index. Read the limitations before you move a hot query into a document.
Comparison
A plain JSONField gives you a dictionary. There is no type, no
validation, no form widget and no admin column, and every query needs the
data__ prefix. JSONStore gives each key a real Django field.
A DecimalField returns a Decimal, and a DateField returns a date. The field validates the value on the way in. A raw document returns the string.
Name the field in Meta.fields, in ModelAdmin.fields, or in list_display. Each field builds its own widget, so no custom form code is necessary.
filter(full_name="Ann Lee") and order_by("hire_date") compile to a key transform on the JSON column. The data__ prefix stays out of your code.
To give a field its own column, remove the jsonstore. prefix and run makemigrations. Then copy the values out of the document in a data migration.
Field types
Each field takes the same arguments as the equivalent field in
django.db.models. This includes max_length,
default, null, blank,
choices, verbose_name and help_text. The
field converts the value when it reads the document.
Keys and layout
A field uses its own name as the key, in a JSON column with the name
data. json_field_name selects the column, and one
model can keep its virtual fields in more than one column. Put the public
values in one column and the private values in another.
json_key selects the key. Give a string for a different key, or
a list for a path. The package makes the intermediate dictionaries, and more
than one field can share a parent key. The key applies to every operation:
reads, writes, filter(), filter(city__isnull=True)
and order_by().
class Person(models.Model): data = models.JSONField(default=dict) full_name = jsonstore.CharField( max_length=250, json_key="name") city = jsonstore.CharField( max_length=100, json_key=("address", "city")) zip_code = jsonstore.CharField( max_length=10, json_key=("address", "zip")) Person(full_name="Ann", city="Paris", zip_code="75001").data # {"name": "Ann", # "address": {"city": "Paris", # "zip": "75001"}}
Relations
jsonstore.ForeignKey keeps the primary key of the related object
under the key <name>_id. The package reads the object at
first access, then keeps it in a cache. A lazy "app.Model"
target and any primary key type are permitted, and
formfield() returns a ModelChoiceField.
OneToOneField operates in the same way in the forward direction.
jsonstore.ManyToManyField keeps a list of primary keys, so there
is no join table. The accessor is a manager with
all, add, remove, set,
clear and count.
The value is in a document, and thus the database has no foreign key
constraint. on_delete has no effect, the target model gets no
reverse accessor, and a join is not available. Query the JSON key instead.
class Book(models.Model): data = models.JSONField(default=dict) title = jsonstore.CharField(max_length=250) author = jsonstore.ForeignKey( Author, null=True) book = Book(title="Pale Fire", author=nabokov) book.data # {"title": …, "author_id": 1} book.author # <Author: Nabokov> post.tags.set([python, django]) post.tags.count() # 2 post.save() # A join is not available. Use the key. Book.objects.filter( data__author_id=nabokov.pk)
Embedded documents
jsonstore.EmbeddedModel declares typed fields and has no table.
EmbeddedField puts one instance in a model, and
EmbeddedListField holds a list of them. Both accept every field
type above, and an embedded model can contain a second one.
The accessor is attached to the stored sub-document. Thus an in-place change
such as product.price.amount = 120 becomes permanent at the next
save(). The list accessor permits an index, a slice, iteration,
len, append, insert and
del.
class Money(jsonstore.EmbeddedModel): amount = jsonstore.IntegerField() currency = jsonstore.CharField( max_length=3, default="USD") class Product(models.Model): data = models.JSONField(default=dict) price = jsonstore.EmbeddedField( Money, null=True) product.price = Money(amount=100) product.data # {"price": {"amount": 100, "currency": "USD"}} product.price.amount # 100 order.lines.append(LineItem(sku="c", qty=3)) order.lines[0].qty = 5 order.save()
One table, several types
Virtual fields operate with proxy models. Each proxy declares the fields of its own type, and every type shares the one JSON column of the base model. Multi-table inheritance is not necessary, and a new subclass adds no table and no migration.
class User(PolymorphicModel, AbstractUser): data = models.JSONField(default=dict) class Client(User): address = jsonstore.CharField(max_length=250) city = jsonstore.CharField(max_length=250) vip = jsonstore.BooleanField() class Meta: proxy = True
Install
pip install django-jsonstore
Do not add an application to INSTALLED_APPS. The package needs
Python 3.10 or later, and Django 5.2, 6.0 or 6.1. Add one
models.JSONField to the model, then declare the virtual fields.
Databases
Django's own JSONField writes the data. Thus the requirements are
the same as the
requirements of Django.
A query on a virtual field becomes a key transform, and the database must be able
to read a key in the document. The suite passes on SQLite, PostgreSQL 17,
MySQL 8.4, MariaDB 11 and Oracle 23ai.
Limitations
A value in a document is outside the reach of the schema. Read this list before you select a document over a column.
jsonstore.ForeignKey can point to a row that no longer exists. on_delete has no effect, and an OneToOneField has no UNIQUE index.select_related, prefetch_related and filter(author__name=...) are not available.instance.save() after the change.order_by() on a numeric field puts 10 before 2. PostgreSQL, MySQL and SQLite sort numerically.EmbeddedListField keeps the order of the list, and ManyToManyField.all() returns the rows in database order.FAQ
No. A virtual field has no column of its own, and makemigrations produces nothing for it. The model needs one models.JSONField, and that column takes one migration. After that, a new field is a code change only.
Yes. A query on a virtual field becomes a key transform on the JSON column. filter(), exclude(), order_by(), values() and values_list() accept the field name. The field has no index, so a filter reads each document. Add a functional index on the JSON column for a hot query.
A plain JSONField gives you a dictionary. There is no type, no validation, no form field and no admin column, and every query needs the data__ prefix. JSONStore gives each key a real Django field. The field converts the value, validates it, builds the form widget, and accepts its own name in a query.
Remove the jsonstore. prefix and run makemigrations. Then write a data migration that copies the values out of the JSON document. No other code changes are necessary, because the field name and the field type stay the same.
The AGPL, with the additional permissions in LICENSE_EXCEPTION. The exception permits use in a project with a license that is not compatible with the AGPL, and a proprietary project is included. Your own code keeps your own license. The condition is that you do not change the source code of this package.