SchemaFactory¶
SchemaFactory.create_schema() builds a ModelSchema at runtime. Use it when
the shape is decided by data rather than written in a class body — a
?fields= query parameter, a permission-dependent projection, an admin tool.
For a shape you know at import time, declare a
ModelSchema — it is clearer and type-checkable.
Building a schema¶
>>> EventTitleSchema = SchemaFactory.create_schema(
... models.Event,
... name='EventTitleSchema',
... fields=['title'],
... skip_registry=True,
... )
>>> EventTitleSchema.__name__
'EventTitleSchema'
>>> list(EventTitleSchema.model_fields)
['title']
The result is an ordinary ModelSchema subclass:
>>> issubclass(EventTitleSchema, ModelSchema)
True
>>> EventTitleSchema.model_validate(models.Event(title='DjangoCon')).model_dump()
{'title': 'DjangoCon'}
Parameters¶
| Parameter | Type | Default | Purpose |
|---|---|---|---|
model |
Django model | — | Required, positional. |
name |
str |
model class name | Name of the generated class. |
fields |
list[str] |
all fields | Allow-list. |
exclude |
list[str] |
None |
Deny-list. |
optional_fields |
list[str] or '__all__' |
None |
Fields made non-required. |
depth |
int |
0 |
Relation nesting depth. |
skip_registry |
bool |
False |
Do not cache or reuse from the registry. |
registry |
SchemaRegister |
global registry | Registry to use. |
Note the name difference: the factory argument is optional_fields, while the
Config option is optional.
>>> PatchSchema = SchemaFactory.create_schema(
... models.Event,
... name='EventPatchSchema',
... fields=['title'],
... optional_fields=['title'],
... skip_registry=True,
... )
>>> PatchSchema.model_fields['title'].is_required()
False
exclude works the same way, and — as with Config — the two are mutually
exclusive:
>>> SummarySchema = SchemaFactory.create_schema(
... models.Event,
... name='EventSummarySchema',
... exclude=['category'],
... skip_registry=True,
... )
>>> list(SummarySchema.model_fields)
['id', 'title']
The registry¶
Generated schemas are cached per model in a global registry. That is what
lets nested schemas resolve when you use depth, but it has a consequence worth
understanding.
The first call for a model registers a schema:
>>> first = SchemaFactory.create_schema(models.Day, name='DayNameSchema', fields=['name'])
>>> list(first.model_fields)
['name']
A later call for the same model returns the cached schema, and the arguments you passed are ignored:
>>> second = SchemaFactory.create_schema(models.Day, name='DayIdSchema', fields=['id'])
>>> second is first
True
>>> list(second.model_fields)
['name']
Pass skip_registry=True for one-off shapes
Building schemas per request without skip_registry=True means the first
shape ever requested is the shape everyone gets — a bug that surfaces only
under a particular request order.
skip_registry=True both skips the cache lookup and avoids registering the
result, so the schema stays private to the caller.
A runtime projection¶
Putting it together — a schema chosen by request input, with the field list checked against an allow-list first:
from django.http import HttpRequest, JsonResponse
from django_modern_schemas import SchemaFactory
from .models import Event
ALLOWED_FIELDS = {'id', 'title'}
def list_events(request: HttpRequest) -> JsonResponse:
requested = set(request.GET.get('fields', '').split(',')) & ALLOWED_FIELDS
schema = SchemaFactory.create_schema(
Event,
name='EventProjection',
fields=sorted(requested) or ['id', 'title'],
skip_registry=True, # per-request shape, never cached
)
results = [schema.model_validate(event).model_dump() for event in Event.objects.all()]
return JsonResponse({'results': results})
Validate the field list yourself
Intersecting with an allow-list, as above, does two jobs: it stops a client
projecting columns you did not mean to expose, and it keeps an unknown field
name from turning into a 500 — the factory raises ConfigError for a name
that is not on the model.
Related guides¶
- ModelSchema — the declarative equivalent
- Relations — what
depthdoes