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)
⚠ Warning (Model Support)
You can now supply a standard Django
Model(not aModelSerializer) inM2MRelationSchema.model. When doing so you must providerelated_schemamanually:PythonM2MRelationSchema( 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_schemais omitted for a plainModel, validation will raise an error. This path is experimental and its behavior or requirements may change without notice.For
ModelSerializermodels nothing changes:related_schemais inferred automatically viagenerate_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