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.
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')]
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.
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
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.
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])).
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.
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).
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/json101 – virtual fields on a plain modelcookbook/embed101 – embedded documents edited as nested formsEvery class below is importable from viewflow.jsonstore and from the
standalone jsonstore package under the same name.