CRUD framework for Django Ninja, sync or async
Serializer-first schemas · Relations · Filtering · Pagination · Auth · Permissions
Documentation · PyPI · Framework Comparison · Performance Benchmarks · Example Project · Issues
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
| Feature | Description | |
|---|---|---|
| ⚡ | Sync and async | Every viewset runs async by default, or sync with execution_mode = "sync" |
| 🐍 | Python CRUD API | create, get, update, destroy, model_dump and their async versions on every serializer |
| 📐 | Schemas | Declare create, update, read and detail with class Schemas and SchemaConfig |
| 🔗 | Relations | Nested reads, relations as ids, and nested writes for child objects |
| 📦 | Bulk operations | Bulk create, update and delete with per-item results |
| 🔍 | Filtering | Filter, search, ordering and pagination mixins |
| 🛡️ | Permissions | Viewset and object-level permission checks, including role-based access |
| 🗑️ | Soft delete | Mark rows as deleted instead of removing them |
| 🔑 | Authentication | JWT bearer and cookie auth, with per-method settings |
| 🪝 | Hooks | Reactive @on_create, @on_update and @on_delete hooks on your models |
| 🧰 | Django admin | Register a ModelSerializer in the admin with @register_admin |
| 🤖 | MCP server | Expose your viewsets as MCP tools |
| 🔒 | Type safety | Generic Serializer and APIViewSet classes for IDE autocomplete |
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.
| Method | Path | What it does |
|---|---|---|
GET |
/api/articles |
List articles, with pagination |
POST |
/api/articles/ |
Create an article |
GET |
/api/articles/{id} |
Get one article |
PATCH |
/api/articles/{id}/ |
Update some fields |
DELETE |
/api/articles/{id}/ |
Delete an article |
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.
Metadata
Release files for django-ninja-aio-crud 3.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_ninja_aio_crud-3.0.1.tar.gz | 10.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_ninja_aio_crud-3.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 10.4 MB
Release files / django_ninja_aio_crud-3.0.1.tar.gz
| Download URL | django_ninja_aio_crud-3.0.1.tar.gz |
|---|---|
| Size | 10.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
08005558e1eb71e8fb7c08a49653521c3aaf9baf16fc3449018b0acfb4d0852b
|
|
BLAKE2b-256 checksum How to use checksums |
c2bb15c5c7d48a3b624f1f3982473b4019ba194bc86e99d1af17efd989cdf189
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-requests/2.34.2
|
Release files / django_ninja_aio_crud-3.0.1-py3-none-any.whl
| Download URL | django_ninja_aio_crud-3.0.1-py3-none-any.whl |
|---|---|
| Size | 136.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4b3e38dd67d0b7eaf1914e969acd7eb576d37c629e84f3e2e5f157cb36e9e9d5
|
|
BLAKE2b-256 checksum How to use checksums |
ff7c72f96eb57b8421fb04bea21d0f7464726894bd25453578f346e5548f3816
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-requests/2.34.2
|