Django JSONStore

Typed access to nested JSON

Expose nested JSON
as Django fields.

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")

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

Use JSON values where Django expects fields

A plain JSONField gives you a dictionary. JSONStore gives selected values a type, validation, a form widget, an admin column and a query name.

Typed values in both directions

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.

Forms and the admin work

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.

The JSON path stays in the model

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.

Whole nested documents

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

Twenty-five field types, and the relations

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.

Stored as a string

Stored as a number

Stored as ISO 8601 text

Stored as a JSON boolean

Stored without loss

Relations, as primary keys

Sub-documents


Keys and layout

Map any path to a field

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

A foreign key in the document

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

Typed access to whole nested 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

Polymorphic models without a second table

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

One command, and no application to register

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

Every backend that Django supports for JSON

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

What the database no longer does for you

A value in a document is outside the reach of the schema. Read this list before you select a document over a column.

FAQ

Questions

Does a jsonstore field need a migration?

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.

Can I filter and sort on a virtual field?

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.

How is this different from a plain JSONField?

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.

What happens when a field must become a real column?

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.

Which license applies?

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.