APIViewSet¶
APIViewSet auto-generates async CRUD endpoints and optional Many-to-Many (M2M) endpoints for a Django Model or a ModelSerializer. It supports dynamic schema generation, per-verb authentication, pagination, list & relation filtering with runtime-built Pydantic schemas, and custom view injection.
Generated CRUD Endpoints¶
| Method | Path | Summary | Response |
|---|---|---|---|
| POST | /{base}/ |
Create Model | 201 schema_out |
| GET | /{base}/ |
List Models | 200 List[schema_out] (paginated) |
| GET | /{base}/{pk} |
Retrieve Model | 200 schema_out |
| PATCH | /{base}/{pk}/ |
Update Model | 200 schema_out |
| DELETE | /{base}/{pk}/ |
Delete Model | 204 No Content |
Notes:
- Retrieve path has no trailing slash; update/delete include a trailing slash.
{base}auto-resolves from model verbose name plural (lowercase) unlessapi_route_pathis provided.- Error responses may use a unified generic schema for codes: 400, 401, 404, 428.
Core Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
model |
ModelSerializer \| Model |
— | Target model (required) |
api |
NinjaAPI |
— | API instance (required) |
schema_in |
Schema \| None |
None (auto) |
Create input schema override |
schema_out |
Schema \| None |
None (auto) |
Read/output schema override |
schema_update |
Schema \| None |
None (auto) |
Update input schema override |
pagination_class |
type[AsyncPaginationBase] |
PageNumberPagination |
Pagination strategy |
query_params |
dict[str, tuple[type, ...]] |
{} |
List endpoint filters definition |
disable |
list[type[VIEW_TYPES]] |
[] |
Disable CRUD views (create,list,retrieve,update,delete,all) |
api_route_path |
str |
"" |
Base route segment |
list_docs |
str |
"List all objects." |
List endpoint description |
create_docs |
str |
"Create a new object." |
Create endpoint description |
retrieve_docs |
str |
"Retrieve a specific object by its primary key." |
Retrieve endpoint description |
update_docs |
str |
"Update an object by its primary key." |
Update endpoint description |
delete_docs |
str |
"Delete an object by its primary key." |
Delete endpoint description |
m2m_relations |
list[M2MRelationSchema] |
[] |
M2M relation configs |
m2m_auth |
list \| None |
NOT_SET |
Default auth for all M2M endpoints (overridden per relation if set) |
Authentication Attributes¶
| Attribute | Type | Default | Description |
|---|---|---|---|
auth |
list \| None |
NOT_SET |
Global fallback auth |
get_auth |
list \| None |
NOT_SET |
Auth for list + retrieve |
post_auth |
list \| None |
NOT_SET |
Auth for create |
patch_auth |
list \| None |
NOT_SET |
Auth for update |
delete_auth |
list \| None |
NOT_SET |
Auth for delete |
Resolution rules:
- Per-verb auth overrides
authwhen notNOT_SET. Nonemakes the endpoint public (no authentication).- M2M endpoints use relation-level auth (
m2m_data.auth) or fall back tom2m_auth.
Automatic Schema Generation¶
If model is a subclass of ModelSerializerMeta:
schema_outis generated fromReadSerializerschema_infromCreateSerializerschema_updatefromUpdateSerializer
Otherwise provide them manually.
List Filtering¶
Define filters for the list view with query_params:
Override handler:
async def query_params_handler(self, queryset, filters: dict):
if filters.get("is_active") is not None:
queryset = queryset.filter(is_active=filters["is_active"])
if filters.get("role"):
queryset = queryset.filter(role=filters["role"])
if filters.get("search"):
from django.db.models import Q
s = filters["search"]
queryset = queryset.filter(Q(username__icontains=s) | Q(email__icontains=s))
return queryset
A dynamic Pydantic model (FiltersSchema) is built with pydantic.create_model from query_params.
Many-to-Many Relations¶
Relations are declared via M2MRelationSchema objects (not tuples). Each schema can include:
model: related Django model or ModelSerializerrelated_name: attribute name on the main model (e.g."tags")path: custom URL segment (optional)auth: list of auth instances (optional)add: enable additions (bool)remove: enable removals (bool)get: enable GET listing (bool)filters: dict of{param_name: (type, default)}for relation-level filteringrelated_schema: optional pre-built schema for the related model (auto-generated if themodelis aModelSerializer)
If path is empty it falls back to the related model verbose name (lowercase plural).
If filters is provided a per-relation filters schema is auto-generated and exposed on the GET relation endpoint:
GET /{base}/{pk}/{related_path}?param=value
Custom filter hook naming convention:
<related_name>_query_params_handler(self, queryset, filters_dict)
Model Support
You can now supply a standard Django Model (not a ModelSerializer) in M2MRelationSchema.model. When doing so you must provide related_schema manually:
M2MRelationSchema(
model=Tag, # plain django.db.models.Model
related_name="tags",
related_schema=TagOut, # a Pydantic/Ninja Schema you define
add=True,
remove=True,
get=True,
)
If related_schema is omitted for a plain Model, validation will raise an error. This path is experimental and its behavior or requirements may change without notice.
For ModelSerializer models nothing changes: related_schema is inferred automatically via generate_related_s().
Example with filters:
class UserViewSet(APIViewSet):
model = User
api = api
m2m_relations = [
M2MRelationSchema(
model=Tag,
related_name="tags",
filters={
"name": (str, "")
}
)
]
async def tags_query_params_handler(self, queryset, filters):
name_filter = filters.get("name")
if name_filter:
queryset = queryset.filter(name__icontains=name_filter)
return queryset
Generated M2M Endpoints (per relation)¶
| Method | Path | Feature |
|---|---|---|
| GET | /{base}/{pk}/{rel_path} |
List related objects (paginated, optional filters) |
| POST | /{base}/{pk}/{rel_path}/ |
Add/remove related objects |
Request bodies:
- Both add & remove enabled:
{ "add": [ids], "remove": [ids] } - Only add:
{ "add": [ids] } - Only remove:
{ "remove": [ids] }
Success/manage response (M2MSchemaOut):
Operations use async managers (aadd, aremove) and run concurrently via asyncio.gather.
M2M Example¶
class ArticleViewSet(APIViewSet):
model = Article
api = api
m2m_relations = [
M2MRelationSchema(model=Tag, related_name="tags"),
M2MRelationSchema(model=Category, related_name="categories", path="article-categories"),
M2MRelationSchema(model=User, related_name="authors", path="co-authors", auth=[AdminAuth()])
]
m2m_auth = [JWTAuth()] # fallback for relations without custom auth
Custom Views¶
Override views() to register extra endpoints:
def views(self):
@self.router.get("/stats/", response={200: GenericMessageSchema})
async def stats(request):
total = await self.model.objects.acount()
return {"message": f"Total: {total}"}
Dynamic View Naming¶
All generated handlers are decorated with @unique_view(...) to ensure stable unique function names (prevents collisions and ensures consistent OpenAPI schema generation). Relation endpoints use explicit names like get_<model>_<rel_path> and manage_<model>_<rel_path>.
Overridable Hooks¶
| Hook | Purpose |
|---|---|
views() |
Register custom endpoints |
query_params_handler(queryset, filters) |
Apply list filters |
<related_name>_query_params_handler(queryset, filters) |
Apply relation-specific filters |
Error Handling¶
All CRUD and M2M endpoints may respond with GenericMessageSchema for error codes: 400 (validation), 401 (auth), 404 (not found), 428 (precondition required).
Performance Tips¶
- Implement
@classmethod async def queryset_request(cls, request)in yourModelSerializerto prefetch related objects. - Use database indexes on filtered fields (
query_paramsand relationfilters). - Keep pagination enabled for large datasets.
- Prefetch reverse relations via
model_util.get_reverse_relations()(already applied in list view). - Limit slice size for expensive searches if needed (
queryset = queryset[:1000]).
Minimal Usage¶
Disable Selected Views¶
class ReadOnlyUserViewSet(APIViewSet):
model = User
api = api
disable = ["create", "update", "delete"]
Authentication Example¶
class UserViewSet(APIViewSet):
model = User
api = api
auth = [JWTAuth()] # global fallback
get_auth = None # list/retrieve public
delete_auth = [AdminAuth()] # delete restricted
Complete M2M + Filters Example¶
class Tag(ModelSerializer):
name = models.CharField(max_length=100)
class ReadSerializer:
fields = ["id", "name"]
class User(ModelSerializer):
username = models.CharField(max_length=150)
tags = models.ManyToManyField(Tag, related_name="users")
class ReadSerializer:
fields = ["id", "username", "tags"]
class UserViewSet(APIViewSet):
model = User
api = api
query_params = {
"search": (str, None)
}
m2m_relations = [
M2MRelationSchema(
model=Tag,
related_name="tags",
filters={"name": (str, "")},
add=True,
remove=True,
get=True
)
]
async def query_params_handler(self, queryset, filters):
if filters.get("search"):
from django.db.models import Q
s = filters["search"]
return queryset.filter(Q(username__icontains=s))
return queryset
async def tags_query_params_handler(self, queryset, filters):
name_filter = filters.get("name")
if name_filter:
queryset = queryset.filter(name__icontains=name_filter)
return queryset