Model Util¶
ModelUtil is an async utility class that provides high-level CRUD operations and serialization management for Django models and ModelSerializer instances.
Overview¶
ModelUtil acts as a bridge between Django Ninja schemas and Django ORM, handling: - Data normalization (input/output) - Relationship resolution (FK/M2M) - Binary field handling (base64 encoding/decoding) - Query optimization (select_related/prefetch_related) - Lifecycle hook invocation (custom_actions, post_create, queryset_request)
Class Definition¶
Parameters:
- model (type[ModelSerializer] | models.Model): Django model or ModelSerializer subclass
Properties¶
model_pk_name¶
Returns the primary key field name.
model_fields¶
Returns a list of all model field names.
util = ModelUtil(User)
print(util.model_fields)
# ["id", "username", "email", "created_at", "is_active"]
schema_out_fields¶
Returns serializable fields (ReadSerializer fields or all model fields).
class User(ModelSerializer):
username = models.CharField(max_length=150)
password = models.CharField(max_length=128)
email = models.EmailField()
class ReadSerializer:
fields = ["id", "username", "email"]
util = ModelUtil(User)
print(util.schema_out_fields)
# ["id", "username", "email"] (password excluded)
serializer_meta¶
Returns the ModelSerializerMeta instance if model uses ModelSerializer.
Core Methods¶
get_object()¶
Fetch single object or queryset with optimized queries.
Signature¶
async def get_object(
request: HttpRequest,
pk: int | str = None,
filters: dict = None,
getters: dict = None,
with_qs_request: bool = True,
) -> ModelSerializer | models.Model | QuerySet
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
request |
HttpRequest |
Required | Current HTTP request |
pk |
int \| str |
None |
Primary key for single object |
filters |
dict |
None |
Queryset filters (field__lookup: value) |
getters |
dict |
None |
Get single object by custom fields |
with_qs_request |
bool |
True |
Apply queryset_request() filtering |
Return Value¶
- If
pkorgettersprovided → Single model instance - Otherwise → QuerySet
Features¶
- Automatic query optimization: Applies
select_related()for ForeignKey andprefetch_related()for reverse relations - Request-based filtering: Respects model's
queryset_request()method - Error handling: Raises
SerializeError(404) if object not found
Examples¶
Get single object by ID:
Get with filters:
active_users = await util.get_object(
request,
filters={"is_active": True, "role": "admin"}
)
# Returns QuerySet: SELECT * FROM user WHERE is_active = TRUE AND role = 'admin'
Get by custom field:
user = await util.get_object(
request,
getters={"email": "john@example.com"}
)
# SELECT * FROM user WHERE email = 'john@example.com'
Without queryset_request filtering:
# Bypass request-based filtering (e.g., for internal operations)
all_users = await util.get_object(request, with_qs_request=False)
With relationships:
class Article(ModelSerializer):
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField(Tag, related_name="articles")
util = ModelUtil(Article)
article = await util.get_object(request, pk=1)
# Automatically executes:
# SELECT * FROM article
# LEFT JOIN user ON article.author_id = user.id
# LEFT JOIN category ON article.category_id = category.id
# WITH prefetch for tags
get_reverse_relations()¶
Discovers reverse relationship field names for prefetch optimization.
Signature¶
Return Value¶
List of reverse relation accessor names.
Example¶
class Author(ModelSerializer):
name = models.CharField(max_length=200)
class Book(ModelSerializer):
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
util = ModelUtil(Author)
reverse_rels = util.get_reverse_relations()
print(reverse_rels) # ["books"]
Detected Relation Types¶
| Django Descriptor | Example | Detected |
|---|---|---|
ReverseManyToOneDescriptor |
author.books |
✓ |
ReverseOneToOneDescriptor |
user.profile |
✓ |
ManyToManyDescriptor |
article.tags |
✓ |
ForwardManyToOneDescriptor |
book.author |
✗ |
ForwardOneToOneDescriptor |
profile.user |
✗ |
Use Case¶
# Avoid N+1 queries when serializing reverse relations
relations = util.get_reverse_relations()
queryset = Author.objects.prefetch_related(*relations)
# Now iterating over authors won't trigger additional queries for books
async for author in queryset:
books = await sync_to_async(list)(author.books.all()) # No query!
parse_input_data()¶
Normalize incoming schema data into model-ready dictionary.
Signature¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
data |
Schema |
Ninja schema instance |
Return Value¶
(payload, customs) where:
- payload (dict): Model-ready data with resolved relationships
- customs (dict): Custom/synthetic fields stripped from payload
Transformations¶
- Strip custom fields → Move to
customsdict - Remove optional None values → Don't update if not provided
- Decode BinaryField → Convert base64 string to bytes
- Resolve FK IDs → Fetch related instances
Examples¶
Basic transformation:
from ninja import Schema
class UserCreateSchema(Schema):
username: str
email: str
bio: str | None = None
data = UserCreateSchema(
username="john_doe",
email="john@example.com",
bio=None # Optional, not provided
)
payload, customs = await util.parse_input_data(request, data)
print(payload)
# {"username": "john_doe", "email": "john@example.com"}
# bio is omitted (None stripped)
print(customs)
# {}
With custom fields:
class User(ModelSerializer):
username = models.CharField(max_length=150)
email = models.EmailField()
class CreateSerializer:
fields = ["username", "email"]
customs = [
("password_confirm", str, None),
("send_welcome_email", bool, True),
]
# Schema includes custom fields
data = UserCreateSchema(
username="john",
email="john@example.com",
password_confirm="secret123",
send_welcome_email=False
)
payload, customs = await util.parse_input_data(request, data)
print(payload)
# {"username": "john", "email": "john@example.com"}
print(customs)
# {"password_confirm": "secret123", "send_welcome_email": False}
With ForeignKey resolution:
class Article(ModelSerializer):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
# Input schema expects IDs
data = ArticleCreateSchema(
title="Getting Started",
author=5, # User ID
category=10 # Category ID
)
payload, customs = await util.parse_input_data(request, data)
print(payload)
# {
# "title": "Getting Started",
# "author": <User instance with id=5>,
# "category": <Category instance with id=10>
# }
With BinaryField (base64):
class Document(ModelSerializer):
name = models.CharField(max_length=200)
file_data = models.BinaryField()
data = DocumentCreateSchema(
name="report.pdf",
file_data="iVBORw0KGgoAAAANSUhEUgA..." # base64 string
)
payload, customs = await util.parse_input_data(request, data)
print(payload)
# {
# "name": "report.pdf",
# "file_data": b'\x89PNG\r\n\x1a\n...' # decoded bytes
# }
Error handling:
try:
payload, customs = await util.parse_input_data(request, bad_data)
except SerializeError as e:
print(e.status_code) # 400
print(e.details)
# {"file_data": "Invalid base64 encoding"}
# or {"author": "User with id 999 not found"}
parse_output_data()¶
Post-process serialized output for consistency.
Signature¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
data |
Schema |
Serialized schema instance |
Return Value¶
Post-processed dictionary ready for API response.
Transformations¶
- Replace nested FK dicts → Actual model instances
- Add
<field>_idkeys → For nested FK references - Flatten nested structures → Consistent response format
Examples¶
Basic FK transformation:
# Before parse_output_data
{
"id": 1,
"title": "Article Title",
"author": {"id": 10, "username": "john_doe"}
}
# After parse_output_data
{
"id": 1,
"title": "Article Title",
"author": <User instance>,
"author_id": 10
}
Nested relationships:
# Before
{
"id": 1,
"author": {
"id": 10,
"profile": {
"id": 5,
"bio": "Developer"
}
}
}
# After
{
"id": 1,
"author": <User instance>,
"author_id": 10,
"profile_id": 5
}
Why is this useful?
Allows accessing relationships directly in subsequent operations:
result = await util.read_s(request, article, ArticleReadSchema)
# Direct access to instances (no additional queries)
author_name = result["author"].username
has_premium = result["author"].is_premium
# Also provides IDs for convenience
author_id = result["author_id"]
verbose_name_path_resolver()¶
Get URL-friendly path segment from model's verbose name plural.
Signature¶
Return Value¶
Slugified plural verbose name.
Example¶
class BlogPost(ModelSerializer):
class Meta:
verbose_name = "blog post"
verbose_name_plural = "blog posts"
util = ModelUtil(BlogPost)
path = util.verbose_name_path_resolver()
print(path) # "blog-posts"
# Used in URL routing:
# /api/blog-posts/
# /api/blog-posts/{id}/
verbose_name_view_resolver()¶
Get display name from model's singular verbose name.
Signature¶
Return Value¶
Capitalized singular verbose name.
Example¶
class BlogPost(ModelSerializer):
class Meta:
verbose_name = "blog post"
util = ModelUtil(BlogPost)
name = util.verbose_name_view_resolver()
print(name) # "Blog post"
# Used in OpenAPI documentation:
# "Create Blog post"
# "Update Blog post"
CRUD Operations¶
create_s()¶
Create new model instance with full lifecycle support.
Signature¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
data |
Schema |
Input schema with creation data |
obj_schema |
Schema |
Output schema for response |
Return Value¶
Serialized created object as dictionary.
Execution Flow¶
1. parse_input_data(data) → (payload, customs)
2. Model.objects.acreate(**payload)
3. custom_actions(customs) [if ModelSerializer]
4. post_create() [if ModelSerializer]
5. read_s(obj, obj_schema)
6. return serialized_dict
Example¶
from ninja import Schema
class UserCreateSchema(Schema):
username: str
email: str
send_welcome: bool = True
class UserReadSchema(Schema):
id: int
username: str
email: str
created_at: datetime
# Create user
data = UserCreateSchema(
username="john_doe",
email="john@example.com",
send_welcome=True
)
result = await util.create_s(request, data, UserReadSchema)
print(result)
# {
# "id": 1,
# "username": "john_doe",
# "email": "john@example.com",
# "created_at": "2024-01-15T10:30:00Z"
# }
With Hooks¶
class User(ModelSerializer):
username = models.CharField(max_length=150)
email = models.EmailField()
class CreateSerializer:
fields = ["username", "email"]
customs = [("send_welcome", bool, True)]
async def custom_actions(self, payload):
if payload.get("send_welcome"):
await send_welcome_email(self.email)
async def post_create(self):
await AuditLog.objects.acreate(
action="user_created",
user_id=self.id
)
# Hooks are automatically invoked
result = await util.create_s(request, data, UserReadSchema)
read_s()¶
Serialize model instance to response dict.
Signature¶
async def read_s(
request: HttpRequest,
obj: ModelSerializer | models.Model,
obj_schema: Schema
) -> dict
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
obj |
ModelSerializer \| models.Model |
Model instance to serialize |
obj_schema |
Schema |
Output schema |
Return Value¶
Serialized object as dictionary.
Execution Flow¶
1. obj_schema.from_orm(obj)
2. schema.model_dump(mode="json")
3. parse_output_data(dumped_data)
4. return processed_dict
Example¶
user = await User.objects.aget(id=1)
result = await util.read_s(request, user, UserReadSchema)
print(result)
# {
# "id": 1,
# "username": "john_doe",
# "email": "john@example.com",
# "created_at": "2024-01-15T10:30:00Z"
# }
With Nested Relations¶
class ArticleReadSchema(Schema):
id: int
title: str
author: UserReadSchema # Nested
article = await Article.objects.select_related('author').aget(id=1)
result = await util.read_s(request, article, ArticleReadSchema)
print(result)
# {
# "id": 1,
# "title": "Getting Started",
# "author": <User instance>,
# "author_id": 10
# }
update_s()¶
Update existing model instance.
Signature¶
async def update_s(
request: HttpRequest,
data: Schema,
pk: int | str,
obj_schema: Schema
) -> dict
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
data |
Schema |
Input schema with update data |
pk |
int \| str |
Primary key of object to update |
obj_schema |
Schema |
Output schema for response |
Return Value¶
Serialized updated object as dictionary.
Execution Flow¶
1. get_object(pk=pk)
2. parse_input_data(data) → (payload, customs)
3. Update obj fields from payload
4. custom_actions(customs) [if ModelSerializer]
5. obj.asave()
6. read_s(obj, obj_schema)
7. return serialized_dict
Example¶
class UserUpdateSchema(Schema):
email: str | None = None
bio: str | None = None
data = UserUpdateSchema(email="newemail@example.com")
result = await util.update_s(request, data, pk=1, obj_schema=UserReadSchema)
print(result)
# {
# "id": 1,
# "username": "john_doe", # unchanged
# "email": "newemail@example.com", # updated
# "bio": "...", # unchanged
# }
Partial Updates¶
Only provided fields are updated:
# Update only email
data = UserUpdateSchema(email="new@example.com")
await util.update_s(request, data, pk=1, UserReadSchema)
# Update only bio
data = UserUpdateSchema(bio="New bio")
await util.update_s(request, data, pk=1, UserReadSchema)
# Update both
data = UserUpdateSchema(email="new@example.com", bio="New bio")
await util.update_s(request, data, pk=1, UserReadSchema)
With Custom Actions¶
class User(ModelSerializer):
class UpdateSerializer:
optionals = [("email", str)]
customs = [("reset_password", bool, False)]
async def custom_actions(self, payload):
if payload.get("reset_password"):
await self.send_password_reset_email()
data = UserUpdateSchema(email="new@example.com", reset_password=True)
await util.update_s(request, data, pk=1, UserReadSchema)
# Email updated AND password reset email sent
delete_s()¶
Delete model instance.
Signature¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
request |
HttpRequest |
Current HTTP request |
pk |
int \| str |
Primary key of object to delete |
Return Value¶
None
Execution Flow¶
Example¶
With Delete Hook¶
class User(ModelSerializer):
def on_delete(self):
logger.info(f"User {self.username} deleted")
cache.delete(f"user:{self.id}")
await util.delete_s(request, pk=1)
# Logs deletion and clears cache
Error Handling¶
ModelUtil raises SerializeError for various failure scenarios:
404 Not Found¶
from ninja_aio.exceptions import SerializeError
try:
user = await util.get_object(request, pk=999)
except SerializeError as e:
print(e.status_code) # 404
print(e.details)
# {"user": "not found"}
400 Bad Request¶
Invalid base64:
try:
data = DocumentCreateSchema(
name="doc.pdf",
file_data="not-valid-base64!!!"
)
await util.create_s(request, data, DocumentReadSchema)
except SerializeError as e:
print(e.status_code) # 400
print(e.details)
# {"file_data": "Invalid base64 encoding"}
Missing related object:
try:
data = ArticleCreateSchema(
title="Test",
author=999 # Non-existent user ID
)
await util.create_s(request, data, ArticleReadSchema)
except SerializeError as e:
print(e.status_code) # 400
print(e.details)
# {"author": "User with id 999 not found"}
Performance Optimization¶
Automatic Query Optimization¶
class Article(ModelSerializer):
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField(Tag, related_name="articles")
util = ModelUtil(Article)
# Single query optimization
article = await util.get_object(request, pk=1)
# Automatically executes:
# SELECT * FROM article
# LEFT JOIN user ON article.author_id = user.id
# LEFT JOIN category ON article.category_id = category.id
# WITH prefetch for tags
# Queryset optimization
articles = await util.get_object(request)
# Automatically adds select_related and prefetch_related
Manual Optimization¶
For complex scenarios, override in ModelSerializer:
class Article(ModelSerializer):
@classmethod
async def queryset_request(cls, request):
return cls.objects.select_related(
'author',
'author__profile', # Deep relation
'category'
).prefetch_related(
'tags',
'comments__author' # Nested prefetch
).only(
'id', 'title', 'content', # Limit fields
'author__username',
'category__name'
)
Integration with APIViewSet¶
ModelUtil is automatically used by APIViewSet:
from ninja_aio.views import APIViewSet
class UserViewSet(APIViewSet):
model = User
api = api
# Internally creates ModelUtil(User)
# All CRUD operations use ModelUtil methods
Complete Example¶
from django.db import models
from ninja_aio.models import ModelSerializer, ModelUtil
from ninja import Schema
from django.http import HttpRequest
# Models
class Author(ModelSerializer):
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
class CreateSerializer:
fields = ["name", "email"]
class ReadSerializer:
fields = ["id", "name", "email"]
class Book(ModelSerializer):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
isbn = models.CharField(max_length=13, unique=True)
cover_image = models.BinaryField(null=True)
is_published = models.BooleanField(default=False)
class CreateSerializer:
fields = ["title", "author", "isbn"]
optionals = [("cover_image", str)] # base64
customs = [("notify_author", bool, True)]
class ReadSerializer:
fields = ["id", "title", "author", "isbn", "is_published"]
class UpdateSerializer:
optionals = [
("title", str),
("is_published", bool),
]
async def custom_actions(self, payload):
if payload.get("notify_author"):
await send_email(
self.author.email,
f"New book created: {self.title}"
)
async def post_create(self):
await AuditLog.objects.acreate(
action="book_created",
book_id=self.id
)
# Usage
async def example(request: HttpRequest):
util = ModelUtil(Book)
# Create
book_data = BookCreateSchema(
title="Django Unleashed",
author=5,
isbn="9781234567890",
cover_image="iVBORw0KGgo...", # base64
notify_author=True
)
created = await util.create_s(request, book_data, BookReadSchema)
# Read
book = await util.get_object(request, pk=created["id"])
serialized = await util.read_s(request, book, BookReadSchema)
# Update
update_data = BookUpdateSchema(is_published=True)
updated = await util.update_s(request, update_data, created["id"], BookReadSchema)
# Delete
await util.delete_s(request, created["id"])
Best Practices¶
-
Always use with async views:
-
Reuse util instances when possible:
-
Let ModelUtil handle query optimization:
-
Handle SerializeError appropriately:
-
Use parse_input_data for custom processing:
See Also¶
- Model Serializer - Define schemas on models
- API ViewSet - High-level CRUD views using ModelUtil