JSONFields in Django 3.0: NoSQL in Postgres
PostgreSQL has had excellent JSON support for years. Django supported it via django.contrib.postgres. Now Django 3.1 brings JSONField to all databases.
JSONField Basics
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
metadata = models.JSONField(default=dict)
tags = models.JSONField(default=list)
settings = models.JSONField(null=True, blank=True)
Store arbitrary JSON structures in your relational database.
Usage
Creating Objects
# Dict data
product = Product.objects.create(
name="Widget",
metadata={
"dimensions": {"width": 10, "height": 20},
"weight": 1.5,
"colors": ["red", "blue"]
},
tags=["electronics", "gadgets"]
)
# Access like regular Python
print(product.metadata["dimensions"]["width"]) # 10
print(product.tags[0]) # "electronics"
Updating JSON Data
product = Product.objects.get(id=1)
# Replace entirely
product.metadata = {"new": "data"}
product.save()
# Partial update (fetch, modify, save)
product.metadata["dimensions"]["height"] = 25
product.save()
Querying JSON Data
Exact Match
Product.objects.filter(metadata={"key": "value"})
Key Existence (PostgreSQL)
Product.objects.filter(metadata__has_key="dimensions")
Product.objects.filter(metadata__has_keys=["width", "height"])
Product.objects.filter(metadata__has_any_keys=["color", "size"])
Nested Lookups
# Access nested values with __
Product.objects.filter(metadata__dimensions__width=10)
Product.objects.filter(tags__0="electronics") # First array element
Contains (PostgreSQL)
Product.objects.filter(metadata__contains={"weight": 1.5})
Product.objects.filter(tags__contains=["electronics"])
Contained By
Product.objects.filter(
tags__contained_by=["electronics", "gadgets", "tools"]
)
Database Support
| Feature | PostgreSQL | MySQL | SQLite | Oracle |
|---|---|---|---|---|
| Basic JSONField | ✅ | ✅ | ✅ | ✅ |
| has_key | ✅ | ❌ | ❌ | ❌ |
| contains | ✅ | ❌ | ❌ | ❌ |
| Nested lookups | ✅ | ✅ | ✅ | ✅ |
| JSON indexing | ✅ | ✅ | ❌ | ❌ |
PostgreSQL has the best support. Others work for basic use.
When to Use JSONField
Good Use Cases
Flexible Metadata:
class Event(models.Model):
name = models.CharField(max_length=100)
metadata = models.JSONField(default=dict)
# Metadata varies by event type
External API Data:
class WebhookPayload(models.Model):
received_at = models.DateTimeField()
payload = models.JSONField()
# Store raw webhook, structure varies
User Preferences:
class UserPreferences(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
settings = models.JSONField(default=lambda: {
"theme": "light",
"notifications": True,
"language": "en"
})
Feature Flags:
class FeatureFlags(models.Model):
flags = models.JSONField(default=dict)
# Usage
flags = FeatureFlags.objects.first()
if flags.flags.get("new_checkout", False):
# Show new checkout
Bad Use Cases
Structured, Queryable Data:
# Don't do this
class Order(models.Model):
data = models.JSONField()
# data = {"customer_id": 1, "total": 100, "items": [...]}
# Do this
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
total = models.DecimalField(...)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
Relationships: JSON can’t enforce foreign key constraints.
Validation
With Validators
from django.core.validators import BaseValidator
class JSONSchemaValidator(BaseValidator):
def compare(self, value, schema):
import jsonschema
try:
jsonschema.validate(value, schema)
except jsonschema.ValidationError as e:
raise ValidationError(str(e))
class Product(models.Model):
metadata = models.JSONField(
validators=[JSONSchemaValidator({
"type": "object",
"properties": {
"dimensions": {"type": "object"},
"weight": {"type": "number"}
}
})]
)
In Forms
from django import forms
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'metadata']
def clean_metadata(self):
data = self.cleaned_data['metadata']
if 'dimensions' not in data:
raise forms.ValidationError("Dimensions required")
return data
Performance Considerations
Indexing (PostgreSQL)
from django.contrib.postgres.indexes import GinIndex
class Product(models.Model):
metadata = models.JSONField()
class Meta:
indexes = [
GinIndex(fields=['metadata']),
]
GIN indexes enable fast contains/exists queries.
Avoid Large JSON
# Bad: Large JSON affects all queries
class Document(models.Model):
content = models.JSONField() # 10MB PDFs parsed to JSON
# Better: Separate table or file storage
class Document(models.Model):
summary = models.JSONField() # Small metadata
content_file = models.FileField() # Large data in files
Admin Integration
from django.contrib import admin
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'get_weight']
def get_weight(self, obj):
return obj.metadata.get('weight', 'N/A')
get_weight.short_description = 'Weight'
Migration from HStoreField
# If you were using HStoreField (string-only)
class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='product',
name='metadata',
field=models.JSONField(default=dict),
),
]
JSONField supports all JSON types, not just strings.
Final Thoughts
JSONField bridges relational and document databases. Use it for truly flexible data—not as a way to avoid schema design.
The rule: if you query it regularly, it probably deserves its own column.
Flexible when needed. Structured when possible.