Typed access to nested JSON
Many applications keep fast-changing business data in a JSONField.
JSONStore maps any path in that document to a typed field. Use it in model
instances, ModelForms, the admin and ORM queries.
import jsonstore class Employee(models.Model): data = models.JSONField(default=dict) full_name = jsonstore.CharField( max_length=250, json_field_name="data", json_key=("profile", "full_name")) hire_date = jsonstore.DateField( null=True, json_field_name="data", json_key=("profile", "hire_date")) employee = Employee(full_name="Ann Lee") employee.data # {"profile": {"full_name": "Ann Lee"}} Employee.objects.filter(full_name="Ann Lee")
from django import forms class EmployeeForm(forms.ModelForm): class Meta: model = Employee fields = ["full_name", "hire_date"] form = EmployeeForm(data={ "full_name": "Ann Lee", "hire_date": "2026-08-17", }) form.is_valid() # True employee = form.save() employee.data # {"profile": { # "full_name": "Ann Lee", # "hire_date": "2026-08-17"}}
from django.contrib import admin @admin.register(Employee) class EmployeeAdmin(admin.ModelAdmin): fields = [ "full_name", "hire_date", ] list_display = [ "full_name", "hire_date", ] # The admin form uses the field types, # validation and widgets from the model.
What changes
Keep the document. Add Django field behavior.
The values stay in the JSON column. Each virtual field adds conversion,
validation and a form widget for one path. ModelForms and ModelAdmin can list
the field by name. Filters, ordering and values() use the same name.
Use this for business data that changes often. It also suits data that belongs to one workflow. Use columns when the database must enforce, index or join it. Read the limitations before you move a hot query into a document.
From document to model API
A plain JSONField gives you a dictionary. JSONStore gives selected
values a type, validation, a form widget, an admin column and a query name.
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 JSON key transforms. The rest of your code uses field names, not document paths. Those names can stay the same if you move the values to regular model columns later.
EmbeddedModel defines a typed document schema. Use EmbeddedField for one document or EmbeddedListField for a list. Embedded models can contain other embedded models.
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
json_field_name selects the JSON column. One model can keep its
virtual fields in more than one column. Put public values in one column and
private values in another.
json_key selects the key. Give a string for a different key or a
tuple for a path of any depth. The package creates intermediate dictionaries.
Several fields can share a parent key. The path applies to reads, writes,
filters and ordering.
class Person(models.Model): data = models.JSONField(default=dict) full_name = jsonstore.CharField( max_length=250, json_field_name="data", json_key="name") city = jsonstore.CharField( max_length=100, json_field_name="data", json_key=("address", "city")) zip_code = jsonstore.CharField( max_length=10, json_field_name="data", 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.filter() on that field matches no row, and values_list() gives an empty string, with no error. Keep long text in a column of its own. The measurements.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.