Responses¶
The response argument on an operation controls what an operation is allowed
to return: it validates the return value, serializes it, and documents its
shape in the OpenAPI schema. This page covers the different shapes response
can take; for how to define the schema classes themselves, see
Schemas.
Basic usage¶
Without a response argument, whatever you return is passed straight to the
renderer (JSON by default) with no validation at all:
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/ping")
def ping(request):
return {"status": "ok"}
Pass a Schema class to validate and filter the output — only the fields
declared on the schema make it into the response, even if the returned object
has more:
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class UserOut(Schema):
id: int
username: str
@api.get("/users/{user_id}", response=UserOut)
def get_user(request, user_id: int):
return {"id": user_id, "username": "sam", "password": "hunter2"}
password is silently dropped — the response is {"id": ..., "username": "sam"}.
For a list endpoint, wrap the schema in list[...]:
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class UserOut(Schema):
id: int
username: str
@api.get("/users", response=list[UserOut])
def list_users(request):
return [{"id": 1, "username": "sam"}, {"id": 2, "username": "alex"}]
Tip
Nested schemas, aliases, resolvers, self-referencing schemas, and
FileField/ImageField handling are all properties of the Schema
class itself, not of response= — see Schemas for those.
Returning querysets¶
You don't need to call .all() or wrap a queryset in list() before
returning it — validating against list[Schema] evaluates it for you:
from ninja import NinjaAPI
from myapp.models import Task
from myapp.schemas import TaskSchema
api = NinjaAPI()
@api.get("/tasks", response=list[TaskSchema])
def tasks(request):
return Task.objects.all()
Async views
This shortcut runs the query synchronously during validation, which
Django forbids inside an async def view. Evaluate the queryset
yourself first, e.g. with asgiref.sync.sync_to_async:
from asgiref.sync import sync_to_async
@api.get("/tasks", response=list[TaskSchema])
async def tasks(request):
return await sync_to_async(list)(Task.objects.all())
See Async Support for more on calling the ORM from async code.
Multiple response schemas¶
An operation often needs more than one possible response — a success body,
plus a different body per error case. Pass response a dict mapping each
HTTP status code to the schema for that code:
from datetime import datetime
from ninja import NinjaAPI, Schema, Status
api = NinjaAPI()
class Token(Schema):
token: str
expires: datetime
class Message(Schema):
message: str
@api.post("/login", response={200: Token, 401: Message, 402: Message})
def login(request, username: str, password: str):
if username != "admin":
return Status(401, {"message": "Unauthorized"})
if password != "hunter2":
return Status(402, {"message": "Payment required"})
return Status(200, {"token": "xyz", "expires": datetime.now()})
Return a Status(status_code, value) to tell Django Ninja which status
you're sending and which of the declared schemas to validate value against.
The status you set is also applied to the actual HTTP response.
Deprecated: returning a (status_code, body) tuple
Older code may return a plain 2-tuple instead of Status(...) — it still
works, but raises a DeprecationWarning and will be removed in a future
release:
Only one status code declared
If response names exactly one status code and it isn't 200 — e.g.
response={201: TaskOut} — that code is used automatically, even for a
plain return task with no Status(...) wrapper.
If a returned status code isn't declared in response at all (and there's no
... fallback — see below), Django Ninja raises a ConfigError.
Response code ranges¶
Repeating the same schema for several codes gets tedious. Group them instead, either with one of the built-in ranges:
from ninja.responses import codes_4xx
@api.post("/login", response={200: Token, codes_4xx: Message})
def login(request, username: str, password: str):
...
from ninja.responses import codes_1xx # 100-101
from ninja.responses import codes_2xx # 200-206
from ninja.responses import codes_3xx # 300-308
from ninja.responses import codes_4xx # 400-412, 416, 418, 425, 429, 451
from ninja.responses import codes_5xx # 500-504
or with your own frozenset of codes:
my_codes = frozenset({410, 429})
@api.post("/login", response={200: Token, my_codes: Message})
def login(request, username: str, password: str):
...
Finally, ... (Ellipsis) matches any status code not otherwise listed
— handy as a catch-all:
@api.get("/status", response={200: Token, ...: Message})
def status(request, code: int):
return Status(code, {"message": "unexpected"})
Note
A ... fallback is excluded from the generated OpenAPI schema and docs
UI, since there's no single status code to describe it under.
Empty responses¶
For a response that has no body — 204 No Content,
for instance — map that status code to None:
@api.post("/tasks/{task_id}", response={204: None})
def delete_task(request, task_id: int):
return Status(204, None)
Returning a Django HttpResponse¶
Return a Django HttpResponse (or any subclass, e.g. redirect(...))
directly and Django Ninja passes it through untouched — no validation,
no response= schema involved:
from django.http import HttpResponse
from django.shortcuts import redirect
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/plain")
def plain_text(request):
return HttpResponse("some data", content_type="text/plain")
@api.get("/old-path")
def moved(request):
return redirect("/new-path")
Related¶
- To set a header, cookie, or status code on the response while still returning your normal data, see Headers, Cookies & Temporal Response.
- For validation-error and exception responses, see Errors & Exception Handling.
- To change how a response body is encoded (JSON by default), see Renderers.
by_alias,exclude_unset,exclude_defaultsandexclude_nonetune how aresponse=schema is serialized — see Operations.