Alternative to Step 1
Define Your Serializer¶
Use Meta-driven Serializer to add API functionality to existing Django models without changing their base class.
What You'll Learn¶
- How
Serializerdiffers fromModelSerializer - Defining schemas with
SchemaModelConfig - Working with relationships via
relations_serializers - Adding custom and computed fields
- Query optimizations with
QuerySet - Implementing lifecycle hooks
- Connecting to
APIViewSet
Prerequisites — Make sure you have Django 4.1+ installed, django-ninja-aio-crud installed, and a Django project set up.
When to Use Serializer
Choose Serializer when you have existing Django models you don't want to modify, want to keep models and API concerns separated, or when multiple teams work on models vs. API layers. If you're starting fresh, consider ModelSerializer instead.
How It Works¶
With ModelSerializer, you embed schema configuration directly on the model. With Serializer, your models stay as plain Django models and you define everything in a separate serializer class:
# models.py — plain Django model
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
# serializers.py — API configuration
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content"]
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "is_published", "created_at"]
)
# models.py — API config on the model
from ninja_aio.models import ModelSerializer
class Article(ModelSerializer):
title = models.CharField(max_length=200)
content = models.TextField()
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class ReadSerializer:
fields = ["id", "title", "content", "is_published", "created_at"]
class CreateSerializer:
fields = ["title", "content"]
Defining Schemas¶
The Serializer uses a nested Meta class with SchemaModelConfig to configure each operation:
Create Schema (schema_in)¶
Defines which fields are accepted when creating an object:
from ninja_aio.models import serializers
from .models import Article
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content"],
optionals=[("is_published", bool)],
)
fields— required fields for creationoptionals— optional fields as(name, type)tuples
Request body:
Read Schema (schema_out)¶
Defines which fields appear in list API responses:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "is_published", "created_at"]
)
Response:
{
"id": 1,
"title": "My Article",
"content": "Content here...",
"is_published": false,
"created_at": "2024-01-15T10:30:00Z"
}
Detail Schema (schema_detail)¶
Optionally return more fields for the retrieve endpoint than the list endpoint:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
# List view: minimal fields
fields=["id", "title", "is_published"]
)
schema_detail = serializers.SchemaModelConfig(
# Detail view: all fields
fields=["id", "title", "content", "is_published", "created_at"],
customs=[("word_count", int, lambda obj: len(obj.content.split()))]
)
Fallback Behavior
If schema_detail is not defined, it falls back to schema_out. If schema_detail is defined, it does not inherit from schema_out — you must specify all fields explicitly.
Update Schema (schema_update)¶
Defines which fields can be updated (PATCH):
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_update = serializers.SchemaModelConfig(
optionals=[
("title", str),
("content", str),
("is_published", bool),
]
)
Request body (partial update):
Complete Schema Definition¶
Putting it all together:
from ninja_aio.models import serializers
from .models import Article
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content"],
optionals=[("is_published", bool)],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "is_published", "created_at"]
)
schema_update = serializers.SchemaModelConfig(
optionals=[
("title", str),
("content", str),
("is_published", bool),
]
)
Working with Relationships¶
Unlike ModelSerializer which auto-resolves nested serializers, Serializer requires you to explicitly declare them via relations_serializers.
ForeignKey Relationships¶
from ninja_aio.models import serializers
from .models import Author, Article
class AuthorSerializer(serializers.Serializer):
class Meta:
model = Author
schema_in = serializers.SchemaModelConfig(
fields=["name", "email"],
optionals=[("bio", str)],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "name", "email", "bio"]
)
schema_update = serializers.SchemaModelConfig(
optionals=[("name", str), ("bio", str)]
)
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content", "author"],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "author", "is_published", "created_at"]
)
schema_update = serializers.SchemaModelConfig(
optionals=[("title", str), ("content", str), ("is_published", bool)]
)
# Explicit nested serialization
relations_serializers = {
"author": AuthorSerializer,
}
class QuerySet:
read = serializers.ModelQuerySetSchema(
select_related=["author"]
)
Response with nested author:
{
"id": 1,
"title": "My Article",
"content": "Content here...",
"author": {
"id": 5,
"name": "John Doe",
"email": "john@example.com",
"bio": "Software developer"
},
"is_published": false,
"created_at": "2024-01-15T10:30:00Z"
}
Reverse Relations¶
Include reverse relations (e.g., an author's articles) using relations_serializers:
class AuthorSerializer(serializers.Serializer):
class Meta:
model = Author
schema_out = serializers.SchemaModelConfig(
fields=["id", "name", "email", "articles"] # reverse related name
)
relations_serializers = {
"articles": ArticleSerializer,
}
String References (Circular Dependencies)¶
When two serializers reference each other, use string references to avoid import errors:
class AuthorSerializer(serializers.Serializer):
class Meta:
model = Author
schema_out = serializers.SchemaModelConfig(
fields=["id", "name", "articles"]
)
relations_serializers = {
"articles": "ArticleSerializer", # String reference
}
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "author"]
)
relations_serializers = {
"author": "AuthorSerializer", # Circular reference works!
}
You can also use absolute import paths for cross-module references:
Relations as IDs¶
For lighter responses, serialize relations as IDs instead of nested objects:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "author", "tags", "category"]
)
relations_serializers = {
"author": AuthorSerializer, # Nested object
"category": CategorySerializer, # Nested object
}
relations_as_id = ["tags"] # Just IDs
Response:
{
"id": 1,
"title": "My Article",
"author": {"id": 5, "name": "John Doe", "email": "john@example.com"},
"category": {"id": 2, "name": "Tutorials", "slug": "tutorials"},
"tags": [1, 2, 5]
}
ManyToMany Relationships¶
from .models import Author, Tag, Article
class TagSerializer(serializers.Serializer):
class Meta:
model = Tag
schema_in = serializers.SchemaModelConfig(fields=["name", "slug"])
schema_out = serializers.SchemaModelConfig(fields=["id", "name", "slug"])
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content", "author"],
optionals=[("tags", list[int])],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "author", "tags", "created_at"]
)
relations_serializers = {
"author": AuthorSerializer,
"tags": TagSerializer,
}
class QuerySet:
read = serializers.ModelQuerySetSchema(
select_related=["author"],
prefetch_related=["tags"],
)
Custom and Computed Fields¶
Custom Fields in Read Schema¶
Add computed fields using customs:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "views", "created_at"],
customs=[
("word_count", int, lambda obj: len(obj.content.split())),
("reading_time", int, lambda obj: max(1, len(obj.content.split()) // 200)),
]
)
Inline Custom Fields¶
You can also define custom fields directly in the fields list as tuples:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=[
"id",
"title",
("word_count", int, 0), # 3-tuple: (name, type, default)
("is_featured", bool), # 2-tuple: (name, type) — required
]
)
Custom Fields in Create Schema¶
Use customs for instruction flags that aren't stored in the database:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content", "author"],
customs=[
("notify_subscribers", bool, True),
("schedule_publish", str, None),
],
)
These are passed to the custom_actions() hook.
Query Optimizations¶
Configure select_related / prefetch_related for automatic optimization:
from ninja_aio.schemas.helpers import ModelQuerySetSchema, ModelQuerySetExtraSchema
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "author", "category"]
)
schema_detail = serializers.SchemaModelConfig(
fields=["id", "title", "content", "author", "category", "tags", "comments"]
)
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author", "category"],
prefetch_related=["tags"],
)
detail = ModelQuerySetSchema(
select_related=["author", "category"],
prefetch_related=["tags", "comments", "comments__author"],
)
queryset_request = ModelQuerySetSchema(
select_related=[],
prefetch_related=["comments"],
)
extras = [
ModelQuerySetExtraSchema(
scope="cards",
select_related=["author"],
prefetch_related=[],
)
]
read— applied to list operationsdetail— applied to retrieve operations (falls back toreadif not defined)queryset_request— applied inside thequeryset_requesthookextras— named scopes available viaQueryUtil.SCOPES
Lifecycle Hooks¶
Serializer supports the same hooks as ModelSerializer, with one key difference: all hooks receive an instance parameter instead of using self:
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content", "author"],
customs=[("notify_author", bool, True)],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "content", "author"]
)
@classmethod
async def queryset_request(cls, request):
"""Filter and optimize queryset per request."""
return cls._meta.model.objects.select_related("author")
async def custom_actions(self, payload, instance):
"""Execute after field assignment, before save."""
if payload.get("notify_author"):
await send_email(instance.author.email, f"Article created: {instance.title}")
async def post_create(self, instance):
"""Execute after instance creation."""
await AuditLog.objects.acreate(
action="article_created",
article_id=instance.id
)
def before_save(self, instance):
"""Sync hook before any save."""
from django.utils.text import slugify
instance.slug = slugify(instance.title)
def after_save(self, instance):
"""Sync hook after any save."""
from django.core.cache import cache
cache.delete(f"article:{instance.id}")
def on_create_before_save(self, instance):
"""Sync hook before creation save only."""
print(f"Creating: {instance.title}")
def on_create_after_save(self, instance):
"""Sync hook after creation save only."""
print(f"Created with ID: {instance.id}")
def on_delete(self, instance):
"""Sync hook after deletion."""
print(f"Deleted: {instance.title}")
Available Hooks¶
| Hook | Type | When Called | Parameters |
|---|---|---|---|
queryset_request(request) |
async | Before queryset building | request |
custom_actions(payload, i) |
async | After field assignment | payload, instance |
post_create(instance) |
async | After first save | instance |
before_save(instance) |
sync | Before any save | instance |
after_save(instance) |
sync | After any save | instance |
on_create_before_save(i) |
sync | Before creation save only | instance |
on_create_after_save(i) |
sync | After creation save only | instance |
on_delete(instance) |
sync | After deletion | instance |
Execution Order
Create: on_create_before_save() → before_save() → save() → on_create_after_save() → after_save() → custom_actions() → post_create()
Update: before_save() → save() → after_save() → custom_actions()
Connecting to APIViewSet¶
Attach your serializer to a ViewSet using serializer_class:
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from .models import Article, Author, Tag
from .serializers import ArticleSerializer, AuthorSerializer, TagSerializer
api = NinjaAIO(title="Blog API", version="1.0.0")
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
@api.viewset(model=Author)
class AuthorViewSet(APIViewSet):
serializer_class = AuthorSerializer
@api.viewset(model=Tag)
class TagViewSet(APIViewSet):
serializer_class = TagSerializer
That's it — full CRUD endpoints are generated automatically, using your serializer's schemas, query optimizations, and lifecycle hooks.
Complete Example¶
Here's a full example with models, serializers, and views:
Complete blog API with Serializer (click to expand)
# models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="articles")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField(Tag, related_name="articles", blank=True)
is_published = models.BooleanField(default=False)
views = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
# serializers.py
from ninja_aio.models import serializers
from ninja_aio.schemas.helpers import ModelQuerySetSchema
from django.utils.text import slugify
from .models import Author, Category, Tag, Article
class AuthorSerializer(serializers.Serializer):
class Meta:
model = Author
schema_in = serializers.SchemaModelConfig(
fields=["name", "email"],
optionals=[("bio", str)],
)
schema_out = serializers.SchemaModelConfig(
fields=["id", "name", "email", "bio"]
)
schema_update = serializers.SchemaModelConfig(
optionals=[("name", str), ("bio", str)]
)
class CategorySerializer(serializers.Serializer):
class Meta:
model = Category
schema_in = serializers.SchemaModelConfig(fields=["name", "slug"])
schema_out = serializers.SchemaModelConfig(fields=["id", "name", "slug"])
class TagSerializer(serializers.Serializer):
class Meta:
model = Tag
schema_in = serializers.SchemaModelConfig(fields=["name"])
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_in = serializers.SchemaModelConfig(
fields=["title", "content", "author", "category"],
optionals=[
("tags", list[int]),
("is_published", bool),
],
customs=[("notify_subscribers", bool, True)],
)
schema_out = serializers.SchemaModelConfig(
fields=[
"id", "title", "slug", "content",
"author", "category", "tags",
"is_published", "views", "created_at",
],
customs=[
("word_count", int, lambda obj: len(obj.content.split())),
("reading_time", int, lambda obj: max(1, len(obj.content.split()) // 200)),
]
)
schema_update = serializers.SchemaModelConfig(
optionals=[
("title", str),
("content", str),
("category", int),
("tags", list[int]),
("is_published", bool),
]
)
relations_serializers = {
"author": AuthorSerializer,
"category": CategorySerializer,
"tags": TagSerializer,
}
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author", "category"],
prefetch_related=["tags"],
)
def before_save(self, instance):
if not instance.slug:
instance.slug = slugify(instance.title)
async def custom_actions(self, payload, instance):
if payload.get("notify_subscribers"):
# Implement your notification logic
pass
async def post_create(self, instance):
print(f"Article created: {instance.title}")
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from .models import Article, Author, Category, Tag
from .serializers import (
ArticleSerializer, AuthorSerializer,
CategorySerializer, TagSerializer,
)
api = NinjaAIO(title="Blog API", version="1.0.0")
@api.viewset(model=Author)
class AuthorViewSet(APIViewSet):
serializer_class = AuthorSerializer
@api.viewset(model=Category)
class CategoryViewSet(APIViewSet):
serializer_class = CategorySerializer
@api.viewset(model=Tag)
class TagViewSet(APIViewSet):
serializer_class = TagSerializer
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
Serializer vs. ModelSerializer¶
| Feature | ModelSerializer | Serializer |
|---|---|---|
| Model class | Custom base class | Plain Django model |
| Configuration | Nested classes on model | Separate Meta class |
| Lifecycle hooks | Instance methods (self) |
Receives instance parameter |
| Relation serializers | Auto-resolved | Explicit via relations_serializers |
| Best for | New projects | Existing projects |
Both approaches fully support nested relations, query optimization, lifecycle hooks, and APIViewSet integration.
What You've Learned¶
- Creating serializers with
SchemaModelConfig - Defining create, read, detail, and update schemas
- Working with ForeignKey, M2M, and reverse relations
- Adding custom computed fields
- Configuring query optimizations
- Implementing lifecycle hooks with
instanceparameter - Connecting serializers to
APIViewSet
-
API Reference