ModelSerializer and HyperlinkedModelSerializer¶
Restflow's model serializers extend DRF's classes with the same
annotation-driven field declaration as Serializer, plus an async
surface for create, update, and validation.
ModelSerializer¶
ModelSerializer mirrors DRF's ModelSerializer. The Meta class
declares the model and the fields to expose, and the serializer
generates DRF fields from the model's columns.
from restflow.serializers import ModelSerializer
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "date_joined"]
Annotation-driven fields layer on top: any annotated name produces a DRF field that overrides whatever the auto-generation would have produced.
from typing import Literal
from restflow.serializers import ModelSerializer, Field
class UserSer(ModelSerializer):
role: Literal["admin", "editor", "viewer"]
extra: str = Field(write_only=True, required=False)
class Meta:
model = User
fields = ["id", "username", "email"]
role and extra are added to the field set without listing them
in Meta.fields.
Auto-merging annotations into Meta.fields¶
When Meta.fields is a list or tuple, the metaclass appends every
annotated name that is not already present.
class UserSer(ModelSerializer):
extra: str = Field(write_only=True)
class Meta:
model = User
fields = ["id", "username"]
# After class creation, UserSer.Meta.fields == ["id", "username", "extra"]
Meta options¶
Every standard DRF Meta option is supported. The most common ones:
| Option | Purpose |
|---|---|
model |
The Django model class. |
fields |
Field names to include. List, tuple, or "__all__". |
exclude |
Field names to exclude. Mutually exclusive with fields. |
read_only_fields |
Names that bypass validate_<name> and are skipped on input. |
extra_kwargs |
Per-field option overrides. |
depth |
Auto-nest related serializers up to this depth. |
validators |
Class-level DRF validators. |
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "date_joined"]
read_only_fields = ["id", "date_joined"]
extra_kwargs = {
"email": {"required": True},
"username": {"min_length": 3, "max_length": 30},
}
extra_kwargs runs against both auto-generated fields and
annotation-driven fields. Settings declared through the Field
sentinel still apply on top.
HyperlinkedModelSerializer¶
HyperlinkedModelSerializer renders related fields and the identity
field as URLs instead of primary keys. It accepts every option that
ModelSerializer does, plus Meta.url_field_name for the identity
field.
from restflow.serializers import HyperlinkedModelSerializer
class ArticleSer(HyperlinkedModelSerializer):
class Meta:
model = Article
fields = ["url", "title", "author", "category"]
url_field_name = "url"
extra_kwargs = {
"url": {"view_name": "article-detail"},
"author": {"view_name": "user-detail"},
"category": {"view_name": "category-detail"},
}
Annotation-driven fields, the async surface, and the Field sentinel
all work identically here.
Source attributes¶
Use source= to map a serializer field to a different attribute on
the model instance.
from rest_framework import serializers
from restflow.serializers import ModelSerializer
class UserSer(ModelSerializer):
full_name = serializers.CharField(source="get_full_name", read_only=True)
company_name = serializers.CharField(source="profile.company.name")
class Meta:
model = User
fields = ["id", "username", "full_name", "company_name"]
The same applies to annotation-driven fields, declared through the
Field sentinel:
from restflow.serializers import ModelSerializer, Field
class UserSer(ModelSerializer):
full_name: str = Field(source="get_full_name", read_only=True)
class Meta:
model = User
fields = ["id", "username"]
source="*" lets a nested serializer flatten or reshape the
instance without needing a real attribute path; see DRF's
Serializer documentation for the full mechanic.
Validation¶
Restflow's model serializers run validators in the same order as DRF:
- Field-level validators declared on the field
(
min_length,validators=[...], etc). - The
validate_<name>hook for each field, if defined. - The top-level
validate(self, attrs)hook. - Class-level
Meta.validators.
validate_¶
The sync hook is called from to_internal_value. The async variant is
called from ato_internal_value. The sync entry point refuses async
hooks with a TypeError whose message names ato_internal_value.
from rest_framework.exceptions import ValidationError
from restflow.serializers import ModelSerializer
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
def validate_username(self, value):
if value.startswith("_"):
raise ValidationError("Cannot start with underscore.")
return value
Async version:
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
async def validate_username(self, value):
exists = await User.objects.filter(username=value).aexists()
if exists:
raise ValidationError("Already taken.")
return value
Calling is_valid() on this serializer raises TypeError because
the user callable is async; call ais_valid() instead.
validate¶
The top-level hook handles cross-field rules. Both sync and async
versions are supported. The async avalidate falls back to
returning attrs unchanged.
class PasswordSer(Serializer):
password: str
password_again: str
def validate(self, attrs):
if attrs["password"] != attrs["password_again"]:
raise ValidationError({"password_again": "Mismatch."})
return attrs
class PasswordSer(Serializer):
password: str
password_again: str
async def avalidate(self, attrs):
if attrs["password"] != attrs["password_again"]:
raise ValidationError({"password_again": "Mismatch."})
return attrs
The sync validate hook is required for is_valid(). The async
avalidate is optional and is awaited from arun_validation. When
both are defined, sync code paths use validate and async code
paths use avalidate.
Async create and update¶
asave awaits acreate for new instances and aupdate for
updates. ModelSerializer ships default implementations of both
that mirror DRF's sync ModelSerializer.create and
ModelSerializer.update logic using the async ORM (acreate,
asave, aset). No override is needed for the common case.
from restflow.serializers import ModelSerializer
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
ser = UserSer(data={"username": "alice", "email": "alice@example.com"})
await ser.ais_valid(raise_exception=True)
user = await ser.asave()
Override acreate or aupdate only when the default logic is not
sufficient:
class UserSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email"]
async def acreate(self, validated_data):
validated_data["created_by"] = self.context["request"].user
return await User.objects.acreate(**validated_data)
async def aupdate(self, instance, validated_data):
send_notification = validated_data.get("email") != instance.email
for attr, value in validated_data.items():
setattr(instance, attr, value)
await instance.asave()
if send_notification:
await notify_email_changed(instance)
return instance
If only the sync create or update is overridden, asave falls
back to it. This keeps existing code working under async views
without a rewrite.
The sync save() still works the same way and refuses async
create/update overrides with a TypeError pointing at asave.
Read-only model serialization¶
Read-only serializers come up often -- for example, when a list endpoint returns a different shape than the create endpoint accepts. The standard tools apply here:
class UserListSer(ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "date_joined"]
read_only_fields = fields
For computed values, declare a SerializerMethodField or an
annotated read-only field through Field: