Django migrations need to serialize Python values into migration files so they can be recreated when migrations run. Understanding how Django serializes values and how to handle custom serialization is crucial for creating robust migrations with complex data types and custom objects.
# Django automatically serializes common Python types in migrations
from django.db import models
from datetime import datetime, date
from decimal import Decimal
import uuid
class ExampleModel(models.Model):
# Simple types - automatically serialized
name = models.CharField(max_length=100, default='Default Name') # String
count = models.IntegerField(default=42) # Integer
price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('19.99')) # Decimal
is_active = models.BooleanField(default=True) # Boolean
created_date = models.DateField(default=date.today) # Date function
# More complex types
unique_id = models.UUIDField(default=uuid.uuid4) # UUID function
data = models.JSONField(default=dict) # Dict function
tags = models.JSONField(default=list) # List function
# Generated migration shows serialized values
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExampleModel',
fields=[
('id', models.AutoField(primary_key=True)),
('name', models.CharField(default='Default Name', max_length=100)),
('count', models.IntegerField(default=42)),
('price', models.DecimalField(decimal_places=2, default=decimal.Decimal('19.99'), max_digits=10)),
('is_active', models.BooleanField(default=True)),
('created_date', models.DateField(default=datetime.date.today)),
('unique_id', models.UUIDField(default=uuid.uuid4)),
('data', models.JSONField(default=dict)),
('tags', models.JSONField(default=list)),
],
),
]
# Django's serialization system handles imports automatically
# Notice how it adds necessary imports at the top of the migration file:
"""
import datetime
import decimal
import uuid
from django.db import migrations, models
"""
# Serialization of complex objects
class ComplexSerializationExample:
"""Examples of complex value serialization"""
@staticmethod
def datetime_serialization():
"""How Django serializes datetime objects"""
from datetime import datetime, timezone
# Specific datetime instance
specific_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
# In migration, this becomes:
# datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='post',
name='published_at',
field=models.DateTimeField(
default=datetime.datetime(2023, 1, 1, 12, 0, tzinfo=datetime.timezone.utc)
),
),
]
@staticmethod
def function_serialization():
"""How Django serializes function references"""
# Function references are serialized as strings
def get_default_status():
return 'draft'
# In model:
status = models.CharField(max_length=20, default=get_default_status)
# In migration, this becomes:
# 'myapp.models.get_default_status'
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='post',
name='status',
field=models.CharField(default='myapp.models.get_default_status', max_length=20),
),
]
@staticmethod
def class_serialization():
"""How Django serializes class references"""
from django.contrib.auth.models import User
# Model references are serialized as strings
author = models.ForeignKey(User, on_delete=models.CASCADE)
# In migration:
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.AddField(
model_name='post',
name='author',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='auth.user'
),
),
]
# Custom serialization for complex objects
from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter
class CustomObject:
"""Custom object that needs special serialization"""
def __init__(self, name, value):
self.name = name
self.value = value
def __eq__(self, other):
return isinstance(other, CustomObject) and self.name == other.name and self.value == other.value
def __repr__(self):
return f"CustomObject(name='{self.name}', value={self.value})"
class CustomObjectSerializer(BaseSerializer):
"""Custom serializer for CustomObject"""
def serialize(self):
"""Serialize CustomObject to migration-safe format"""
# Return tuple of (serialized_value, imports)
return (
f"myapp.utils.CustomObject(name={repr(self.value.name)}, value={repr(self.value.value)})",
{"from myapp.utils import CustomObject"}
)
# Register custom serializer
MigrationWriter.register_serializer(CustomObject, CustomObjectSerializer)
# Usage in model
class ModelWithCustomDefault(models.Model):
custom_field = models.CharField(
max_length=100,
default=CustomObject('default', 42)
)
# Generated migration will use custom serializer
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='modelwithcustomdefault',
name='custom_field',
field=models.CharField(
default=myapp.utils.CustomObject(name='default', value=42),
max_length=100
),
),
]
# Serializing callable objects
class CallableSerializer:
"""Handle serialization of callable objects"""
@staticmethod
def serialize_lambda():
"""Lambdas cannot be serialized - use named functions instead"""
# BAD: Lambda functions cannot be serialized
# default_func = lambda: 'default'
# GOOD: Named function can be serialized
def get_default_value():
return 'default'
return get_default_value
@staticmethod
def serialize_method():
"""Instance methods need special handling"""
class MyClass:
def get_default(self):
return 'default'
@classmethod
def get_class_default(cls):
return 'class_default'
@staticmethod
def get_static_default():
return 'static_default'
# Only static methods and class methods can be easily serialized
# Instance methods require the instance to exist
return MyClass.get_static_default
@staticmethod
def serialize_partial_function():
"""Serialize functools.partial objects"""
from functools import partial
def create_slug(prefix, title):
return f"{prefix}-{title.lower().replace(' ', '-')}"
# Partial function
blog_slug = partial(create_slug, 'blog')
# Custom serializer for partial functions
class PartialSerializer(BaseSerializer):
def serialize(self):
func = self.value.func
args = self.value.args
keywords = self.value.keywords
func_name = f"{func.__module__}.{func.__name__}"
return (
f"functools.partial({func_name}, *{args!r}, **{keywords!r})",
{"import functools", f"from {func.__module__} import {func.__name__}"}
)
MigrationWriter.register_serializer(partial, PartialSerializer)
return blog_slug
# Handling complex data structures
class ComplexDataSerialization:
"""Serialize complex data structures"""
@staticmethod
def serialize_nested_dict():
"""Serialize nested dictionaries with custom objects"""
complex_default = {
'settings': {
'theme': 'dark',
'notifications': True,
'custom_obj': CustomObject('nested', 123)
},
'metadata': {
'version': '1.0',
'created': datetime.now()
}
}
# Django will recursively serialize nested structures
# Custom objects within nested structures need their own serializers
return complex_default
@staticmethod
def serialize_custom_collections():
"""Serialize custom collection types"""
from collections import OrderedDict, defaultdict
# OrderedDict serialization
ordered_data = OrderedDict([
('first', 1),
('second', 2),
('third', 3)
])
class OrderedDictSerializer(BaseSerializer):
def serialize(self):
items = list(self.value.items())
return (
f"collections.OrderedDict({items!r})",
{"import collections"}
)
MigrationWriter.register_serializer(OrderedDict, OrderedDictSerializer)
# defaultdict serialization
def default_list():
return []
default_dict_data = defaultdict(default_list)
class DefaultDictSerializer(BaseSerializer):
def serialize(self):
default_factory = self.value.default_factory
items = dict(self.value)
if default_factory:
factory_name = f"{default_factory.__module__}.{default_factory.__name__}"
return (
f"collections.defaultdict({factory_name}, {items!r})",
{"import collections", f"from {default_factory.__module__} import {default_factory.__name__}"}
)
else:
return (
f"collections.defaultdict(None, {items!r})",
{"import collections"}
)
MigrationWriter.register_serializer(defaultdict, DefaultDictSerializer)
return ordered_data, default_dict_data
class CircularReferenceHandling:
"""Handle circular references in serialization"""
@staticmethod
def avoid_circular_references():
"""Strategies to avoid circular references"""
# Problem: Circular reference between models
class Author(models.Model):
name = models.CharField(max_length=100)
# Don't set default to a specific Post instance
# featured_post = models.ForeignKey('Post', null=True, default=???)
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# Solution 1: Use None as default and handle in application logic
class AuthorSafe(models.Model):
name = models.CharField(max_length=100)
featured_post = models.ForeignKey('Post', null=True, blank=True, default=None)
# Solution 2: Use a function that returns None initially
def get_default_featured_post():
return None
class AuthorWithFunction(models.Model):
name = models.CharField(max_length=100)
featured_post = models.ForeignKey(
'Post',
null=True,
blank=True,
default=get_default_featured_post
)
# Solution 3: Use string reference instead of object reference
class AuthorWithString(models.Model):
name = models.CharField(max_length=100)
featured_post_id = models.IntegerField(null=True, blank=True, default=None)
@staticmethod
def handle_self_references():
"""Handle self-referential models"""
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
default=None # Safe default
)
# For more complex self-reference defaults
def get_root_category():
"""Get or create root category"""
# This function will be called at runtime, not migration time
try:
return Category.objects.get(name='Root', parent=None)
except Category.DoesNotExist:
return None
class CategoryWithDefault(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
default=get_root_category
)
# Serializing third-party objects
class ThirdPartyObjectSerialization:
"""Handle serialization of third-party library objects"""
@staticmethod
def serialize_external_objects():
"""Serialize objects from external libraries"""
# Example: Serializing a custom enum
from enum import Enum
class StatusEnum(Enum):
DRAFT = 'draft'
PUBLISHED = 'published'
ARCHIVED = 'archived'
class EnumSerializer(BaseSerializer):
def serialize(self):
enum_class = self.value.__class__
enum_value = self.value.value
return (
f"{enum_class.__module__}.{enum_class.__name__}({enum_value!r})",
{f"from {enum_class.__module__} import {enum_class.__name__}"}
)
MigrationWriter.register_serializer(StatusEnum, EnumSerializer)
# Example: Serializing a custom configuration object
class Config:
def __init__(self, **kwargs):
self.settings = kwargs
def __eq__(self, other):
return isinstance(other, Config) and self.settings == other.settings
class ConfigSerializer(BaseSerializer):
def serialize(self):
settings = self.value.settings
return (
f"myapp.config.Config(**{settings!r})",
{"from myapp.config import Config"}
)
MigrationWriter.register_serializer(Config, ConfigSerializer)
return StatusEnum.DRAFT, Config(debug=True, cache_timeout=300)
@staticmethod
def serialize_file_objects():
"""Handle file and path objects"""
from pathlib import Path
import os
# Path objects
default_path = Path('/tmp/uploads')
class PathSerializer(BaseSerializer):
def serialize(self):
path_str = str(self.value)
return (
f"pathlib.Path({path_str!r})",
{"import pathlib"}
)
MigrationWriter.register_serializer(Path, PathSerializer)
# Environment variables (not directly serializable)
def get_upload_path():
return os.environ.get('UPLOAD_PATH', '/tmp/uploads')
# Use function instead of direct os.environ access
upload_path_func = get_upload_path
return default_path, upload_path_func
# Dynamic serialization
class DynamicSerialization:
"""Handle dynamic serialization scenarios"""
@staticmethod
def conditional_serialization():
"""Serialize values conditionally"""
def get_conditional_default():
"""Return different defaults based on environment"""
import os
if os.environ.get('DJANGO_ENV') == 'production':
return 'production_default'
elif os.environ.get('DJANGO_ENV') == 'testing':
return 'test_default'
else:
return 'development_default'
# This function will be serialized as a string reference
return get_conditional_default
@staticmethod
def version_aware_serialization():
"""Serialize values that depend on Django version"""
def get_version_aware_default():
"""Return different defaults based on Django version"""
import django
if django.VERSION >= (4, 0):
return {'new_format': True}
else:
return {'legacy_format': True}
return get_version_aware_default
@staticmethod
def lazy_serialization():
"""Handle lazy objects and promises"""
from django.utils.functional import lazy
from django.utils.translation import gettext_lazy as _
# Lazy translation strings
lazy_string = _('Default message')
# Django handles lazy objects automatically in most cases
# But for custom lazy objects, you might need custom serialization
def get_lazy_value():
return str(_('Computed at runtime'))
lazy_func = lazy(get_lazy_value, str)
class LazySerializer(BaseSerializer):
def serialize(self):
# For lazy objects, serialize the underlying function
if hasattr(self.value, '_setupfunc'):
func = self.value._setupfunc
return (
f"django.utils.functional.lazy({func.__module__}.{func.__name__}, str)",
{
"from django.utils.functional import lazy",
f"from {func.__module__} import {func.__name__}"
}
)
else:
# Fallback to string representation
return (repr(str(self.value)), set())
# Register for custom lazy types if needed
# MigrationWriter.register_serializer(YourLazyType, LazySerializer)
return lazy_string, lazy_func
class SerializationTroubleshooting:
"""Common serialization issues and solutions"""
@staticmethod
def debug_serialization_errors():
"""Debug common serialization errors"""
# Error 1: "Cannot serialize" error
def fix_cannot_serialize_error():
"""Fix 'Cannot serialize' errors"""
# Problem: Using lambda or local function
# BAD:
# default_func = lambda: 'default'
# Solution: Use module-level function
def get_default_value():
return 'default'
# Problem: Using instance method
# BAD:
# class MyClass:
# def get_default(self):
# return 'default'
# obj = MyClass()
# default_func = obj.get_default
# Solution: Use static method or function
class MyClass:
@staticmethod
def get_default():
return 'default'
default_func = MyClass.get_default
return get_default_value, default_func
# Error 2: Import errors in migrations
def fix_import_errors():
"""Fix import errors in generated migrations"""
# Problem: Function not importable at migration time
# Solution: Ensure function is in a stable module location
# Create utils module for migration-safe functions
# myapp/utils.py
def migration_safe_default():
"""Function that will be available during migrations"""
return 'safe_default'
# Use in model
# default=migration_safe_default
return migration_safe_default
# Error 3: Circular import in migrations
def fix_circular_imports():
"""Fix circular import issues"""
# Problem: Model references create circular imports
# Solution: Use string references
# Instead of importing the model:
# from myapp.models import RelatedModel
# default_related = RelatedModel.objects.first
# Use string reference:
def get_default_related():
from django.apps import apps
RelatedModel = apps.get_model('myapp', 'RelatedModel')
return RelatedModel.objects.first()
return get_default_related
return fix_cannot_serialize_error, fix_import_errors, fix_circular_imports
@staticmethod
def validate_serialization():
"""Validate that objects can be serialized"""
def test_serialization(value):
"""Test if a value can be serialized for migrations"""
from django.db.migrations.writer import MigrationWriter
try:
# Try to serialize the value
serialized, imports = MigrationWriter.serialize(value)
print(f"✓ Serialization successful:")
print(f" Value: {serialized}")
print(f" Imports: {imports}")
return True, serialized, imports
except Exception as e:
print(f"✗ Serialization failed: {e}")
return False, None, None
def create_serialization_test_suite():
"""Create test suite for serialization"""
test_values = [
# Basic types
'string',
42,
3.14,
True,
None,
# Collections
[1, 2, 3],
{'key': 'value'},
(1, 2, 3),
# Functions
len, # Built-in function
str.upper, # Method
# Custom objects
CustomObject('test', 42),
# Datetime objects
datetime.now(),
date.today(),
]
results = []
for value in test_values:
success, serialized, imports = test_serialization(value)
results.append({
'value': value,
'type': type(value).__name__,
'success': success,
'serialized': serialized,
'imports': imports
})
return results
return test_serialization, create_serialization_test_suite
@staticmethod
def create_migration_safe_defaults():
"""Create migration-safe default values"""
# Safe patterns for default values
safe_patterns = {
'simple_values': {
'string': 'default_string',
'integer': 42,
'boolean': True,
'none': None,
},
'callable_defaults': {
'current_time': timezone.now,
'uuid': uuid.uuid4,
'empty_dict': dict,
'empty_list': list,
},
'custom_functions': {
'computed_default': lambda: 'computed_value', # BAD - use named function
'safe_computed': 'myapp.utils.get_computed_default', # GOOD
}
}
# Template for migration-safe utility functions
utility_functions_template = '''
# myapp/utils.py - Migration-safe utility functions
from datetime import datetime, timezone
from decimal import Decimal
import uuid
def get_default_status():
"""Get default status for new records"""
return 'draft'
def get_current_timestamp():
"""Get current timestamp - migration safe"""
return datetime.now(timezone.utc)
def get_default_config():
"""Get default configuration dictionary"""
return {
'theme': 'light',
'notifications': True,
'language': 'en'
}
def generate_unique_slug():
"""Generate unique slug for new records"""
return f"item-{uuid.uuid4().hex[:8]}"
def get_default_price():
"""Get default price as Decimal"""
return Decimal('0.00')
# Custom object factory
def create_default_metadata():
"""Create default metadata object"""
return {
'version': '1.0',
'created': datetime.now(timezone.utc).isoformat(),
'tags': []
}
'''
return safe_patterns, utility_functions_template
Understanding Django's serialization system ensures your migrations work correctly with complex data types and custom objects. Proper serialization prevents migration errors and maintains compatibility across different environments and Django versions.
Squashing Migrations
Migration squashing combines multiple migrations into a single, optimized migration file. This process helps maintain a clean migration history, improves performance, and reduces complexity in long-running projects. Understanding when and how to squash migrations is essential for maintaining a healthy Django project.
Supporting Multiple Django Versions
Creating migrations that work across multiple Django versions requires understanding version differences, compatibility patterns, and migration system evolution. This section covers strategies for maintaining migration compatibility while supporting different Django releases.