Note: This is beta release which is still testing - we encorage you to test this release as well and provide feedback
What's New
Idempotent Router(s)
Routers are now reusable and can be mounted to multiple APIs or multiple times within the same API. Decorators, auth, tags, and throttle settings are fully isolated between mounts.
router = Router(tags=["shared"])
@router.get("/items")
def list_items(request):
return [{"id": 1}]
# Mount same router to multiple APIs
api_v1 = NinjaAPI(urls_namespace="v1")
api_v1.add_router("/", router)
api_v2 = NinjaAPI(urls_namespace="v2")
api_v2.add_router("/", router) # !!! Before this was giving an errorCursor Pagination
New CursorPagination class for stable pagination over frequently changing datasets. Uses base64-encoded cursor tokens instead of offsets, ensuring consistent results even when items are added or removed.
from ninja.pagination import paginate, CursorPagination
@api.get("/events", response=list[EventSchema])
@paginate(CursorPagination, ordering=("-created",), page_size=20)
def list_events(request):
return Event.objects.all()Status Return
New Status class for explicitly returning HTTP status codes. Replaces the old tuple syntax (status_code, body) which is now deprecated.
from ninja import Status
@api.post("/login", response={200: Token, 401: Message})
def login(request, payload: Auth):
if not valid:
return Status(401, {"message": "Unauthorized"})
return Status(200, {"token": token})Skip Re-validation
When returning a Pydantic model instance that already matches the response schema, Django Ninja now skips redundant validation and directly serializes — a nice performance boost.
@api.get("/user", response=UserOut)
def get_user(request):
return UserOut(id=1, name="John") # skips re-validationStreaming Responses (JSONL & SSE)
First-class streaming support with automatic schema validation for each chunk. Supports both JSONL and Server-Sent Events formats.
from ninja.streaming import JSONL, SSE
@api.get("/items", response=JSONL[Item])
def stream_items(request):
for i in range(100):
yield {"name": f"item-{i}", "price": float(i)}
@api.get("/events", response=SSE[Item])
async def stream_events(request):
async for item in get_items():
yield itemDetails
- Idempotent router(s) by @vitalik in #1622
- Cursor pagination by @janrito in #1657
- Status return, Skip revalidation by @vitalik in #1684
- Streaming improvement by @vitalik in #1685
- python 3.14 compatibility (namespace annotations) by @vitalik in #1688
- Propose docs clarifications for view and operational decorators by @martinsvoboda in #1689
- Optional Union fix by @Nuung in #1690
New Contributors
Full Changelog: v1.5.3...v1.6.0b1