Overview¶
This page explains what each public API is for, how a schema is built, and — just as importantly — what the library will not do for you.
The public API¶
| Object | Use it when |
|---|---|
Schema |
You want a plain Pydantic model that reads attributes off arbitrary objects. No Django model involved. |
ModelSchema |
You want fields generated from a Django model. This is the main entry point. |
ModelSchema[Model] |
Same thing, parameterized, so create(), update(), and save() are typed as returning Model. |
SchemaFactory |
You need a schema built at runtime, when the field list is not known at import time. |
Source |
A field's value lives at a dotted attribute path, or under a different name. |
MethodSource |
A field's value comes from calling a zero-argument model method. |
SourceResolver |
You want to resolve a path against an object without building a schema. |
SourceResolutionError |
You are catching a failed Source resolution. |
All of them are importable from the package root:
>>> from django_modern_schemas import (
... MethodSource,
... ModelSchema,
... Schema,
... SchemaFactory,
... Source,
... SourceResolutionError,
... SourceResolver,
... )
How a schema is built¶
When you declare a ModelSchema subclass, the work happens at class creation
time, in the metaclass — not on every validation. In order:
Config.modelis read. Without it, aConfigErroris raised.- The model's concrete fields are collected. Reverse relations
(
ManyToOneRel,ManyToManyRel) are skipped unless named explicitly. fields/excludenarrow that set. Setting both is aConfigError.- Each remaining Django field is converted to a
(python_type, FieldInfo)pair carrying type, default, title, description andmax_length. - Fields listed in
optional— plus the primary key — are made non-required. - Any annotation you declared by hand on the class wins over the generated one.
That last step is what makes Source work: your annotation replaces the
generated field entirely.
>>> class HandWrittenEventSchema(ModelSchema):
... title: str # overrides the generated CharField mapping
... class Config:
... model = models.Event
... fields = ['title']
>>> HandWrittenEventSchema.model_fields['title'].metadata
[]
Compare with the generated version, which carries the Django metadata:
>>> class GeneratedEventSchema(ModelSchema):
... class Config:
... model = models.Event
... fields = ['title']
>>> GeneratedEventSchema.model_fields['title'].metadata
[MaxLen(max_length=100)]
Reading Django instances¶
ModelSchema and Schema both set from_attributes=True, and validation runs
through an internal DjangoGetter. So model_validate() accepts a Django
instance, a mapping, or any object with the right attributes:
>>> class CategorySchema(ModelSchema):
... class Config:
... model = models.Category
... fields = ['name']
>>> CategorySchema.model_validate(models.Category(name='Python')).name
'Python'
>>> CategorySchema.model_validate({'name': 'Python'}).name
'Python'
Boundaries¶
These are deliberate design decisions, not gaps to work around.
The library does not plan queries
Nothing here inspects your querysets or adds select_related() /
prefetch_related() on your behalf. If a schema reaches across a relation,
you are responsible for loading it efficiently — otherwise you get one query
per instance. See Relations → Query planning is yours.
Nested writes are application-specific
create() and update() write flat fields. A schema containing a nested
schema raises NotImplementedError rather than guessing the order of
writes, how to match existing children, or what to do with orphans. Override
the method to express your own rule — see
Persistence → Nested models.
Source is read-only
Source and MethodSource fields are excluded from create() and
update(). They describe how to read a value, not where to store it.
Typical flow¶
- Define a
ModelSchemaand select the fields exposed at your boundary. - Call
model_validate()with a request mapping or a Django instance. - Use
model_dump()for output, ormodel_json_schema()for an API contract. - For flat fields, use
create()orupdate(instance)when the schema represents a write.
Relationship to Ninja Schema¶
The conversion layer began from Ninja Schema's approach to turning Django fields into Pydantic types. The differences that matter in use:
- Configuration is a nested
Configclass, validated eagerly at class creation. Source/MethodSourceare first-class metadata, resolved by a dedicatedSourceResolverwith explicit rules about collections.- Persistence helpers (
create(),update(),save()) ship with the schema.
Full attribution is on the Credits page.