django-ninja-aio-crud builds complete REST APIs on top of Django Ninja. You say which fields each operation reads and writes, register a viewset, and get create, list, retrieve, update and delete endpoints with validation, pagination and interactive docs. The same serializer is also a CRUD API you can call from Python, in sync or async code.
Features
Install
pip install django-ninja-aio-crud
Requires Python 3.10-3.14, Django 5.2 or 6.0, and Django Ninja 1.7.x.
Quick start
Define the model. A ModelSerializer is a normal Django model with a Schemas
class that says which fields each operation reads and writes.
# blog/models.py
from django.db import models
from ninja_aio import ModelSerializer, SchemaConfig
class Article(ModelSerializer):
title = models.CharField(max_length=200)
body = models.TextField()
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Schemas:
create = SchemaConfig(
fields=["title", "body"],
optionals=[("is_published", bool)],
)
update = SchemaConfig(
optionals=[("title", str), ("body", str), ("is_published", bool)],
)
read = SchemaConfig(
fields=["id", "title", "body", "is_published", "created_at"],
)
Create the API and add the URLs:
# blog/api.py
from ninja_aio import NinjaAIO, APIViewSet
from .models import Article
api = NinjaAIO(title="Blog API")
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
pass
# project/urls.py
from django.urls import path
from blog.api import api
urlpatterns = [
path("api/", api.urls),
]
Run python manage.py runserver and open http://localhost:8000/api/docs.
Keep your models as they are
Use a Serializer when you don't want to change your model's base class:
# blog/serializers.py
from ninja_aio import Serializer, SchemaConfig
from .models import Article
class ArticleSerializer(Serializer[Article]):
class Meta:
model = Article
class Schemas:
create = SchemaConfig(fields=["title", "body"])
update = SchemaConfig(optionals=[("title", str), ("body", str)])
read = SchemaConfig(fields=["id", "title", "body"])
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
Sync or async
Viewsets are async by default. To serve the same endpoints with regular sync
views, set execution_mode:
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
execution_mode = "sync"
The model is also a CRUD API you can call in scripts, tasks and tests:
# Sync
article = Article.create({"title": "Draft", "body": "..."})
article = Article.update(article, {"is_published": True})
data = Article.model_dump(article)
# Async
article = await Article.acreate({"title": "Draft", "body": "..."})
article = await Article.aupdate(article, {"is_published": True})
data = await Article.amodel_dump(article)
Custom actions
from ninja_aio import APIViewSet, action, on
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
@on("publish")
async def publish(self, request, obj):
obj.is_published = True
await obj.asave(update_fields=["is_published"])
return await Article.amodel_dump(obj)
@action(detail=False, url_path="stats")
async def stats(self, request):
return {"total": await Article.objects.acount()}
This adds POST /api/articles/{id}/publish and GET /api/articles/stats.
Validators
Add Pydantic validators to a CreateValidators or UpdateValidators class:
from pydantic import field_validator
class Article(ModelSerializer):
# fields and Schemas as above
class CreateValidators:
@field_validator("title")
@classmethod
def title_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("Title cannot be blank")
return value.strip()
Invalid input returns 422 with the error message.
Upgrading from 2.x
Version 3 replaces the inner ReadSerializer/CreateSerializer classes and
Meta.schema_in/schema_out with a single class Schemas, adds sync
viewsets and the Python CRUD API. Follow the
migration guide to upgrade.
Links
Support
If you find this project useful, consider giving it a star or supporting development:
License
MIT License. See LICENSE.