JSONFields in Django 3.0: NoSQL in Postgres

python backend django database

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

FeaturePostgreSQLMySQLSQLiteOracle
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.

All posts