JSON Storage

The viewflow.jsonstore module provides a set of virtual Django Model fields that store their data inside a single JSON database column. This is suitable for storing simple business data, quick prototypes without requiring DB migrations, and replacing multi-table inheritance joins.

Note

These fields are maintained as a standalone package, django-jsonstore, which Viewflow depends on and installs for you. viewflow.jsonstore re-exports it, so from viewflow import jsonstore and a plain import jsonstore give you the very same classes – use whichever reads better. This page documents both.

Quick start

from viewflow import jsonstore
from django import forms
from django.contrib import admin
from django.db import models

class Employee(models.Model):
    data = JSONField(default=dict)
    full_name = jsonstore.CharField(max_length=250)
    hire_date = jsonstore.DateField()
    salary = jsonstore.DecimalField(max_digits=10, decimal_places=2)

The resulting model functions like a typical Django model. All virtual fields are available for constructing ModelForms, Viewsets, and Admin interfaces.

class EmployeeForm(forms.ModelForm):
    class Meta:
        model = Employee
        fields = ['full_name', 'hire_date', 'salary']

@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
    list_display = ['full_name', 'hire_date']
    fields = ['full_name', ('hire_date', 'salary')]

Custom keys and nesting

By default a field is stored under its own name inside a JSON column named data. Both are configurable.

json_field_name selects which JSON column holds the value, so one model can spread its virtual fields across several columns:

class Employee(models.Model):
    public = models.JSONField(default=dict)
    private = models.JSONField(default=dict)

    full_name = jsonstore.CharField(max_length=250)                       # -> public
    ssn = jsonstore.CharField(max_length=20, json_field_name="private")   # -> private

json_key overrides the key used inside that column. Pass a string for a custom key, or a list/tuple for a nested path – intermediate dicts are created automatically, and several fields can share a parent:

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

The custom key / nested path is used everywhere: reading and writing, filtering (Person.objects.filter(city="Paris"), filter(city__isnull=True)) and ordering (order_by("city")). json_key works with every field type, including ForeignKey and ManyToManyField.

Polymorphic models

JSON Store plays well with proxy models. Additional virtual fields allows to model different real-life objects, without involving multi-table inheritance.

from django.contrib.auth.models import AbstractUser
from viewflow import jsonstore

class User(PolymorphicModel, AbstractUser):
    data = jsonstore.JSONField(null=True, default=dict)

class Client(User):
    address = jsonstore.CharField(max_length=250)
    zip_code = jsonstore.CharField(max_length=250)
    city = jsonstore.CharField(max_length=250)
    vip = jsonstore.BooleanField()

    class Meta:
        proxy = True

Foreign keys

Besides primitive fields, JSON Store can hold a reference to another model with jsonstore.ForeignKey. The related object’s primary key is stored in the JSON column under <name>_id; instance.<name> returns the related object (loaded lazily and cached) and instance.<name>_id the raw pk.

from viewflow import jsonstore
from django.db import models

class Book(models.Model):
    data = models.JSONField(default=dict)
    title = jsonstore.CharField(max_length=250)
    author = jsonstore.ForeignKey(Author, null=True, blank=True)

book = Book(title="Viewflow", author=some_author)
book.data        # {"title": "Viewflow", "author_id": some_author.pk}
book.author      # <Author: ...>
book.author_id   # some_author.pk

Like the other JSON Store fields it is virtual (no database column, no migration) and its formfield() is a ModelChoiceField, so it works in ModelForms, Viewsets and the Admin. The target model’s primary key type does not matter – an integer or a UUIDField pk is stored in its JSON-native form and coerced back on read.

Because the value lives in a JSON document there is no database-level foreign key constraint: on_delete has no effect, no reverse accessor is added to the target model, and there is no join support. Query it through the JSON key instead:

Book.objects.filter(data__author_id=some_author.pk)

ORM traversals such as filter(author__name=...) and select_related are not available.

jsonstore.OneToOneField is available too and behaves the same on the forward side; being document-based it has no UNIQUE constraint, so the one-to-one nature is a modelling convention rather than an enforced invariant.

Many-to-many

jsonstore.ManyToManyField keeps a many-to-many relation as a list of primary keys inside the JSONField – no join table. instance.<name> returns a manager with the familiar related-manager API.

from viewflow import jsonstore
from django.db import models

class Post(models.Model):
    data = models.JSONField(default=dict)
    tags = jsonstore.ManyToManyField(Tag)

post.tags.set([tag1, tag2])   # data == {"tags": [tag1.pk, tag2.pk]}
post.tags.add(tag3)
post.tags.remove(tag1)
list(post.tags.all())         # [<Tag: ...>, ...]
post.tags.count()

formfield() yields a ModelMultipleChoiceField, so it works in ModelForms, Viewsets and the Admin; a selection made through a form is persisted for you.

The manager mutates the in-memory document only, so after add / remove / set you must instance.save() to persist – exactly like the other JSON Store fields. There is no join table, through model or reverse accessor; all() is a filter(pk__in=...) queryset (database order). Target models with a UUIDField primary key are supported. Membership queries go through the JSON key and depend on the database’s JSON support (e.g. on PostgreSQL Post.objects.filter(data__tags__contains=[tag.pk])).

Embedded models

For structured sub-data, declare a schema-only jsonstore.EmbeddedModel – a “virtual model” with typed fields but no database table of its own – and embed an instance in a host model with jsonstore.EmbeddedField. It is stored as a nested JSON document.

from viewflow import jsonstore
from django.db import models

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, blank=True)

product.price = Money(amount=100, currency="USD")
product.data          # {"price": {"amount": 100, "currency": "USD"}}
product.price.amount  # 100

EmbeddedModel accepts every scalar/typed JSON Store field (UUIDField, DateField, … serialize at depth), honors json_key inside the document, and can itself contain an EmbeddedField for nested documents. Reading product.price returns an instance bound to the stored sub-document, so product.price.amount = 120 followed by product.save() persists – the same in-memory-then-save contract as the other JSON Store fields. EmbeddedField also accepts json_key and json_field_name to place the document anywhere in the parent. Query the nested values through the raw JSON key, e.g. Product.objects.filter(data__price__amount=100).

For a list of embedded documents use jsonstore.EmbeddedListField – the embedded-document analogue of ManyToManyField:

class Order(models.Model):
    data = models.JSONField(default=dict)
    lines = jsonstore.EmbeddedListField(LineItem)

order.lines = [LineItem(sku="a", qty=1), LineItem(sku="b", qty=2)]
order.lines.append(LineItem(sku="c", qty=3))
order.lines[0].qty = 5          # in-place edit, persisted on save()
order.data                      # {"lines": [{...}, {...}, {...}]}

instance.<name> is a mutable, list-like accessor (indexing, iteration, len, append, insert, del) of embedded instances; each element is bound to its stored document so in-place edits are saved. It accepts json_key / json_field_name too.

Editing embedded documents as forms

Embedded documents plug into viewflow.forms nested forms. Build a form from an EmbeddedModel with EmbeddedModelForm, then edit a single document as a nested form (EmbeddedFormField) and a list of them as inlines (EmbeddedFormSetField) inside a viewflow.forms.ModelForm – all persisted to the one JSON column.

from viewflow import forms as vforms

class AddressForm(vforms.EmbeddedModelForm):
    class Meta:
        model = Address

class LineItemForm(vforms.EmbeddedModelForm):
    class Meta:
        model = LineItem

class InvoiceForm(vforms.ModelForm):
    billing = vforms.EmbeddedFormField(AddressForm)          # nested form
    lines = vforms.EmbeddedFormSetField(LineItemForm, extra=2, can_delete=True)

    class Meta:
        model = Invoice
        fields = ["number", "customer"]

Saving the InvoiceForm writes the nested address and the line items into the model’s JSON column. A runnable demo lives in cookbook/embed101 (mounted at /embedded/ in the demo site, with a Django admin showing the raw JSON).

Supported databases

PostgreSQL 12+, MySQL 8+, MariaDB 10.5+, Oracle 21c+ and SQLite 3.38+.

Note

On MariaDB and Oracle, order_by() on a numeric virtual field sorts the extracted value as text – 10 comes before 2. PostgreSQL, MySQL and SQLite sort numerically.

Django’s own JSONField does the storage, so the requirements match Django’s. Querying a virtual field compiles to a key transform on the JSON column, so the database has to be able to index into the document.

Cookbook

  • cookbook/json101 – virtual fields on a plain model
  • cookbook/embed101 – embedded documents edited as nested forms

API

Every class below is importable from viewflow.jsonstore and from the standalone jsonstore package under the same name.

class
viewflow.jsonstore.BigIntegerField
class
viewflow.jsonstore.BinaryField
class
viewflow.jsonstore.BooleanField
class
viewflow.jsonstore.CharField
class
viewflow.jsonstore.DateField
class
viewflow.jsonstore.DateTimeField
class
viewflow.jsonstore.DecimalField
class
viewflow.jsonstore.DurationField
class
viewflow.jsonstore.EmailField
class
viewflow.jsonstore.EmbeddedField
class
viewflow.jsonstore.EmbeddedListField
class
viewflow.jsonstore.EmbeddedModel
class
viewflow.jsonstore.FilePathField
class
viewflow.jsonstore.FloatField
class
viewflow.jsonstore.ForeignKey
class
viewflow.jsonstore.IntegerField
class
viewflow.jsonstore.IPAddressField
class
viewflow.jsonstore.GenericIPAddressField
class
viewflow.jsonstore.ManyToManyField
class
viewflow.jsonstore.NullBooleanField
class
viewflow.jsonstore.OneToOneField
class
viewflow.jsonstore.PositiveBigIntegerField
class
viewflow.jsonstore.PositiveIntegerField
class
viewflow.jsonstore.PositiveSmallIntegerField
class
viewflow.jsonstore.SlugField
class
viewflow.jsonstore.SmallIntegerField
class
viewflow.jsonstore.TextField
class
viewflow.jsonstore.TimeField
class
viewflow.jsonstore.URLField
class
viewflow.jsonstore.UUIDField