Skip to content

Persistence

A validated schema can write itself back through the ORM with three methods:

Method Effect
create() Creates a new row via the model's default manager.
update(instance, partial=False) Assigns fields onto an existing instance and saves it.
save(instance=None, partial=None) Creates or updates, depending on what it is given — including the instance the schema was validated from.

All three return the saved Django instance. Declare the schema as ModelSchema[Event] and that return value is typed as Event rather than being opaque to your type checker — see ModelSchema[Model].

models.py
class Event(models.Model):
    title = models.CharField(max_length=100)
    category = models.OneToOneField(Category, null=True, on_delete=models.SET_NULL)

    class Meta:
        app_label = 'examples'

    def display_title(self) -> str:
        return f'Event: {self.title}'

create()

create() takes the validated data and calls Model._default_manager.create():

>>> class EventSchema(ModelSchema):
...     class Config:
...         model = models.Event
...         exclude = ['category']
>>> event = EventSchema.model_validate({'title': 'DjangoCon'}).create()
>>> event.pk is not None
True
>>> event.title
'DjangoCon'

The return value is a real Django instance, so it is immediately usable:

>>> models.Event.objects.filter(title='DjangoCon').count()
1

Writing a foreign key

Set the relation by primary key. The schema dumps with by_alias=True, so the value reaches the manager as Django's category_id:

>>> class EventWithCategorySchema(ModelSchema):
...     class Config:
...         model = models.Event
>>> category = models.Category.objects.create(name='Python')
>>> created = EventWithCategorySchema.model_validate(
...     {'title': 'PyCon', 'category': category.pk}
... ).create()
>>> created.category_id == category.pk
True

create() detects that category is a relation and hands the ORM category_id, which is the only form Django accepts for a primary key.

OneToOneField is unique

Event.category is a OneToOneField, so a second Event pointing at the same Category raises an IntegrityError — that is Django enforcing the relation, not a schema problem.

Writing a many-to-many

Many-to-many values cannot be passed to create() — the relation needs a saved row to attach to — so they are applied right after the instance exists:

models.py
class Day(models.Model):
    name = models.CharField(max_length=20)

    class Meta:
        app_label = 'examples'


class Week(models.Model):
    name = models.CharField(max_length=20)
    days = models.ManyToManyField(Day)

    class Meta:
        app_label = 'examples'
>>> class WeekSchema(ModelSchema):
...     class Config:
...         model = models.Week
...         fields = ['name', 'days']
>>> monday = models.Day.objects.create(name='Monday')
>>> tuesday = models.Day.objects.create(name='Tuesday')
>>> week = WeekSchema.model_validate({'name': 'Week 1', 'days': [monday.pk, tuesday.pk]}).create()
>>> [day.name for day in week.days.all()]
['Monday', 'Tuesday']

If a schema field is not a model field

A writable field that the model's manager does not accept raises a TypeError naming the model and suggesting the fix — make the field read-only, or override create(). See the errors reference.

update()

update() assigns each dumped field onto the instance and saves it:

>>> event = models.Event.objects.create(title='DjangoCon')
>>> updated = EventSchema.model_validate({'id': event.pk, 'title': 'PyCon'}).update(event)
>>> models.Event.objects.get(pk=event.pk).title
'PyCon'

The instance you passed in is the instance returned — it is mutated in place:

>>> updated is event
True

Partial updates

By default every field on the schema is written, including fields that fell back to their default. Compare the two modes on a schema where rating was never supplied:

>>> class ProfilePatchSchema(ModelSchema):
...     class Config:
...         model = models.SpeakerProfile
...         fields = ['full_name', 'rating']
...         optional = ['full_name', 'rating']

Unset fields are written, overwriting the stored value with the default:

>>> profile = models.SpeakerProfile.objects.create(uuid=uuid4(), full_name='Ada', rating=9.5)
>>> _ = ProfilePatchSchema.model_validate({'full_name': 'Ada Lovelace'}).update(profile)
>>> models.SpeakerProfile.objects.get(pk=profile.pk).rating is None
True

Only fields explicitly present in the input are written:

>>> profile = models.SpeakerProfile.objects.create(uuid=uuid4(), full_name='Ada', rating=9.5)
>>> _ = ProfilePatchSchema.model_validate({'full_name': 'Ada Lovelace'}).update(profile, partial=True)
>>> refreshed = models.SpeakerProfile.objects.get(pk=profile.pk)
>>> refreshed.full_name, refreshed.rating
('Ada Lovelace', 9.5)

Use partial=True for PATCH endpoints

With partial=False, a schema built from a sparse payload will blank out every column the client did not mention — and on a NOT NULL column that is an IntegrityError rather than a silent overwrite. partial=True uses exclude_unset=True, so absent keys stay absent.

save()

save() picks the operation for you:

>>> schema = EventSchema.model_validate({'title': 'New Event'})
>>> created = schema.save()             # no instance -> create
>>> created.pk is not None
True
>>> existing = models.Event.objects.create(title='Old title')
>>> _ = EventSchema.model_validate({'title': 'New title'}).save(existing)
>>> models.Event.objects.get(pk=existing.pk).title
'New title'

The resolution order is: an internal _object if one is set, then the instance argument, then create().

Validating from an instance

model_validate() remembers what it was given. When the input is a Django model instance, the schema keeps it as its _object, so a later save() updates that row instead of inserting a second one:

>>> stored = models.Event.objects.create(title='Draft')
>>> schema = EventSchema.model_validate(stored)
>>> schema.title = 'Published'
>>> saved = schema.save()
>>> saved.pk == stored.pk
True
>>> models.Event.objects.get(pk=stored.pk).title
'Published'

This is what makes the read → edit → write round trip work without carrying the instance around by hand — the schema you validated is already bound to the row it came from.

Validating a mapping binds nothing, so save() falls through to create():

>>> EventSchema.model_validate({'title': 'Brand new'})._object is None
True

The binding survives, the values do not

_object is only the target of the write. The fields written come from the schema, so editing the schema and calling save() is the whole update — you never touch the instance directly.

Source fields are skipped

Source and MethodSource fields are read-only and never written — see Source fields are never written.

>>> class EventSourceSchema(ModelSchema):
...     category_name: Annotated[str, Source('category.name')]
...     class Config:
...         model = models.Event
...         fields = ['title']
>>> created = EventSourceSchema.model_validate(
...     {'title': 'DjangoCon', 'category_name': 'Python'}
... ).create()
>>> created.title
'DjangoCon'
>>> created.category is None
True

Nested models

A schema containing a nested schema does not write itself. The library cannot know whether the nested object should be created or looked up, in what order the writes must happen, or what to do with children that disappeared — so it raises NotImplementedError and asks you to say. Express the rule by overriding the method:

schemas.py
class EventNestedWriteSchema(ModelSchema):
    category: CategorySchema

    class Config:
        model = Event
        fields = ['title']

    def create(self, *args, **kwargs):
        category, _ = Category.objects.get_or_create(name=self.category.name)
        return Event.objects.create(title=self.title, category=category)

    def update(self, instance, partial=False, **kwargs):
        category, _ = Category.objects.get_or_create(name=self.category.name)
        instance.title = self.title
        instance.category = category
        instance.save()
        return instance

Both create() and update() behave this way. If the nested object is only ever read, mark it with Source instead and the problem disappears — source fields are skipped on write.

Validation happens before the database

Persistence runs on an already-validated schema, so constraints declared on the Django field are enforced before any SQL is issued. Event.title is max_length=100, and SQLite would happily have stored a longer string:

>>> schema = EventSchema.model_validate({'title': 'DjangoCon'})
>>> schema.title
'DjangoCon'

A payload violating that limit never reaches create() — it is rejected by model_validate() with a Pydantic ValidationError, which is the error you return to the client as a 400.