In Django, dynamic models means changing what fields a model has without hand-writing and deploying a migration each time — for example, letting an admin add a new field straight from the Django admin. The honest answer in 2026: Django models map directly to database tables, so adding a real column always requires a schema change (a migration). You cannot safely create true columns on every HTTP request in production. What you can do is pick the right pattern: a built-in JSONField for schema-less attributes (usually the right answer), EAV when end users must define fields as data, or programmatic migrations / schema_editor when you genuinely need real columns generated by code.
Key takeaways
- Django maps models to database tables, so a new column always needs a migration — there is no safe "add a real field per request" in production.
- For most cases, use the built-in
models.JSONField(since Django 3.1) plus a registry of allowed keys validated in forms. - Use EAV (e.g.
django-eav2) only when non-developers must define fields as data; expect ugly joins and no database constraints. - Generate real columns from code with custom migrations or
connection.schema_editor()only in build/admin tools — never in the request/response cycle. type('MyModel', (models.Model,), {...})can build a model class at runtime, but it diverges from your migration history and is not multi-process safe.
What are dynamic models in Django?
A Django model is a Python class that maps to exactly one database table; each field maps to a column. The word "dynamic" gets used for two very different things, and conflating them causes most of the pain:
- Schema-less data — store arbitrary key/value attributes that vary per row, without changing the table structure.
JSONFieldand EAV cover this. - Real schema change at runtime — actually run
ALTER TABLEto add a genuine column, decided by code or an admin action. This needs a migration (or the migration machinery), with all the locking and multi-process implications that follow.
Whenever you want a column the database itself enforces (types, NOT NULL, foreign keys, uniqueness, indexes), you need DDL — a migration. The old trick some 2010s tutorials suggested, dropping and recreating the database to "see" a new field, destroys data and is never acceptable in production. Modern Django gives you migrations for safe, reviewable, reversible schema changes; see our guide to writing custom migrations in Django.
How do the approaches compare?
There is no single winner — match the approach to who defines the fields and whether you need to query and constrain them.
| Approach | How it works | Queryable? | DB-level constraints? | When to use |
|---|---|---|---|---|
| Concrete field + migration | Add the field in code, run migrate |
Native column, full SQL + indexes | Yes (NOT NULL, FK, unique) | Default choice; field set known at build time |
JSONField + key registry |
One jsonb column holds arbitrary keys | Yes on Postgres via __key lookups (+GIN index) |
No — validate in forms | Schema varies per row or admin-defined keys |
EAV (django-eav2) |
Field defs and values stored as rows in side tables | Yes, but via slow multi-joins | No — app-level only | End users must define fields as data; sparse attributes |
Programmatic migration / schema_editor |
Generate AddField / ALTER TABLE from code |
Native column once created | Yes | Build tools, per-tenant SaaS schema; not in request path |
type() runtime model |
Build a model class + create_model() |
Native column | Yes | Code-gen, tests, throwaway models; not for prod requests |
Approach 1: JSONField for schema-less attributes (recommended)
Since Django 3.1, models.JSONField is built in and works on PostgreSQL (backed by jsonb), MySQL, SQLite and Oracle. It is usually the right answer when "fields" vary per row or are admin-defined, yet you still want one stable table and decent query performance.
# catalog/models.py
from django.db import models
# A registry of attributes admins are allowed to set, with light validation.
ALLOWED_ATTRIBUTES = {
"color": str,
"weight_grams": int,
"is_fragile": bool,
}
class Product(models.Model):
name = models.CharField(max_length=200)
# One jsonb column holds all the flexible, schema-less attributes.
attributes = models.JSONField(default=dict, blank=True)
def __str__(self):
return self.nameOn PostgreSQL you can query into the JSON with __ key lookups, and index hot keys with a GIN index for speed:
# Exact key match (uses jsonb on Postgres)
Product.objects.filter(attributes__color="red")
# Nested keys and containment
Product.objects.filter(attributes__dimensions__width__gt=10)
Product.objects.filter(attributes__contains={"is_fragile": True})
# Order or annotate by a JSON key
from django.db.models.fields.json import KeyTextTransform
Product.objects.annotate(
color=KeyTextTransform("color", "attributes")
).order_by("color")JSONField has no database-level constraints, so validate admin input against your registry in a form. Pair it with a custom manager to keep query logic tidy — see Django model managers and properties.
# catalog/forms.py
from django import forms
from .models import Product, ALLOWED_ATTRIBUTES
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ["name", "attributes"]
def clean_attributes(self):
data = self.cleaned_data["attributes"] or {}
for key, value in data.items():
if key not in ALLOWED_ATTRIBUTES:
raise forms.ValidationError(f"Unknown attribute: {key}")
expected = ALLOWED_ATTRIBUTES[key]
if not isinstance(value, expected):
raise forms.ValidationError(
f"{key} must be {expected.__name__}, got {type(value).__name__}"
)
return dataThis gives admins free-form "fields" while keeping one table, real query support, and validation in one place. Add a GinIndex on attributes (via Meta.indexes) if you filter on JSON keys often.
Approach 2: EAV (Entity-Attribute-Value)
EAV stores field definitions and values as rows in side tables, so non-developers can create new "fields" as data — no DDL, no deploy. The maintained library today is django-eav2 (a Jazzband fork of the old django-eav); the original mvpdev/django-eav shown in many 2010s tutorials is unmaintained and Python 2 era.
pip install django-eav2# settings.py
INSTALLED_APPS = [
# ...
"eav",
]
# models.py
import eav
from django.db import models
class Patient(models.Model):
name = models.CharField(max_length=120)
eav.register(Patient) # attaches an "eav" attribute managerExpose the dynamic attributes in the admin just like normal fields by using the EAV admin classes:
# admin.py
from django.contrib import admin
from eav.forms import BaseDynamicEntityForm
from eav.admin import BaseEntityAdmin
from .models import Patient
class PatientAdminForm(BaseDynamicEntityForm):
model = Patient
class PatientAdmin(BaseEntityAdmin):
form = PatientAdminForm
admin.site.register(Patient, PatientAdmin)Admins can now define attributes (the "fields") and set per-record values straight from the admin. Under the hood, each value is linked to its record with a GenericForeignKey — the same mechanism explained in understanding GenericForeignKey in Django. After setup, apply the schema with python manage.py migrate (not the long-removed syncdb).
EAV trade-offs
- Pros: end users add fields with zero deploys; good for sparse, rarely-queried attributes (medical records, specs that differ per product category).
- Cons: queries become multi-join and slow; you lose database constraints (types,
NOT NULL, FKs, uniqueness); reporting and ORM ergonomics suffer. Reach for EAV only whenJSONFieldgenuinely cannot model the requirement.
Approach 3: Programmatic migrations and schema_editor (advanced, risky)
If you truly need real columns created by code — say a SaaS that provisions per-tenant fields — you can drive Django's schema machinery yourself. This is powerful and dangerous: treat it as a build or admin operation, never something that runs inside a normal web request.
Technique 1 — a normal migration with an AddField operation:
# catalog/migrations/0004_add_warranty.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("catalog", "0003_product")]
operations = [
migrations.AddField(
model_name="product",
name="warranty_months",
field=models.PositiveIntegerField(default=12),
),
]Technique 2 — call the management commands from code (e.g. a guarded admin action or script):
from django.core.management import call_command
call_command("makemigrations", "catalog")
call_command("migrate", "catalog")Technique 3 — add a column directly with schema_editor. Note you must also add the field to the model class in Python, or the ORM will not know about it:
from django.db import connection, models
from catalog.models import Product
field = models.CharField(max_length=100, null=True)
field.set_attributes_from_name("nickname")
with connection.schema_editor() as schema_editor:
schema_editor.add_field(Product, field)And the classic "build a model class at runtime" trick with type() plus create_model():
from django.db import connection, models
attrs = {
"__module__": "catalog.models",
"name": models.CharField(max_length=100),
"score": models.IntegerField(default=0),
}
RuntimeModel = type("RuntimeModel", (models.Model,), attrs)
with connection.schema_editor() as schema_editor:
schema_editor.create_model(RuntimeModel)Why this is risky in production
- Locking:
ALTER TABLEtakes locks. Adding aNULLable column (or, on PostgreSQL 11+, a column with a constant default) is fast and metadata-only, but other changes can rewrite the table and block writes. - Not multi-process safe: a model class built with
type()lives in one Python process. Your other Gunicorn/Uvicorn workers — and other servers — will not have it until they restart. - Migration state diverges: columns created by
schema_editorare not recorded in your migration history, so a latermakemigrationssees a mismatch and your schema drifts from source control. - Concurrency: two requests adding the same field race each other.
For anything user-facing, prefer JSONField. If you must generate real columns, do it through reviewed migrations — our custom migrations guide covers RunPython, data migrations and reversibility.
Approach 4: Third-party dynamic-model libraries
Libraries such as django-dynamic-model (rvinzent/django-dynamic-models) and django-mutant let you define models, fields and the underlying schema at runtime, persist the definitions in the database, and manage the real columns for you. They can save work for genuine runtime-schema products, but evaluate maintenance status, Django 5.x compatibility, and how they handle the multi-process and locking issues above before adopting one. For most teams, JSONField plus a key registry is simpler and safer.
How should you let admins add fields safely?
- Default to
JSONField+ a validated key registry; it covers the vast majority of "let admins add fields" requests. - Use real migrations for fields you know at build time; keep them small, reviewed and reversible.
- Reserve EAV for true user-defined-schema products with sparse, rarely-filtered data.
- Keep runtime DDL out of the request cycle; run it as a background or admin job with locking awareness.
- If you only need behaviour changes rather than new columns, a proxy model or custom managers may be all you need.
Need help designing a schema that stays flexible without painting you into a corner? MicroPyramid has shipped 50+ projects in 12+ years (since 2014) and offers Django development services to architect dynamic, future-ready data models.
Frequently Asked Questions
Can I add a field to a Django model without a migration?
Not as a real database column. Django maps each model field to a table column, so a genuine new column requires DDL — a migration. If you want admin-defined fields without migrations, store them in a JSONField or use an EAV library; both keep the table structure fixed while letting the data hold arbitrary attributes.
What is the best way to add dynamic fields in Django in 2026?
For most applications, the built-in models.JSONField (available since Django 3.1) plus a registry of allowed keys validated in a form. It keeps one stable table, supports key-based queries on PostgreSQL, and avoids the join pain of EAV. Use real migrations when the field set is known at build time.
Is JSONField or EAV better for dynamic attributes?
JSONField is simpler, faster, and easier to query, so prefer it. Choose EAV only when non-developers must define new fields themselves as data and you can tolerate multi-join queries and the loss of database constraints. EAV suits sparse, rarely-filtered attributes; JSONField wins almost everywhere else.
Can I create a Django model class at runtime?
Yes — type('MyModel', (models.Model,), {...}) builds a model class dynamically, and connection.schema_editor().create_model() can create its table. But the class exists only in the current process, so other workers will not see it, and the table is not tracked in your migration history. It is fine for code generation, tests or throwaway models, not for production request handling.
How do I add a database column from code with schema_editor?
Build a field instance, call field.set_attributes_from_name('your_field'), then call schema_editor.add_field(Model, field) inside with connection.schema_editor() as schema_editor:. You must also add the field to the model class in Python, mind table locks on large tables, and keep this out of the normal request path.
Why did old tutorials use django-eav and syncdb?
Older Django (pre-1.9) used manage.py syncdb and tutorials reached for mvpdev/django-eav because the framework had no migrations and no JSONField. Both are obsolete now: use manage.py migrate, the maintained django-eav2 fork if you need EAV, and JSONField for most schema-less needs.