In Django Ninja, how does a dict of status codes passed to response= work, and how is the return value's schema chosen?
answer
- one schema per status code
- the view says which code
- Status wrapper replaces the tuple
- undeclared code is a config error
basics
~20 sresponse={201: BookingOut, 409: Message} maps each status to a schema; the view returns Status(409, body) to choose one, and Ninja validates and filters the body through that schema. Returning an undeclared status raises ConfigError, a 500.
solid answer
~40 s`response=` takes either one schema, meaning `{200: schema}`, or a dict from status code to schema. Keys can be ints or frozensets of codes such as `ninja.responses.codes_4xx`, and a `None` value declares an empty body, as for 204. The view picks the code by returning `Status(code, body)` from `ninja`; in 1.7 a plain `(code, body)` tuple still works but emits a `DeprecationWarning`. Without a `Status`, the code is 200 - unless the dict has exactly one key, in which case that key is used. Ninja then validates the body against that code's schema and dumps only the declared fields, so extra attributes such as a password hash never leak. A code missing from the dict raises `ConfigError`, which the client sees as a 500. Returning an `HttpResponse` bypasses all of it.
code
python · 22 linesfrom ninja import NinjaAPI, Schema, Status
from ninja.responses import codes_4xx
api = NinjaAPI()
class Message(Schema):
message: str
@api.post("/bookings", response={201: BookingOut, 409: Message})
def create_booking(request, payload: BookingIn):
if Booking.objects.filter(room_id=payload.room_id, check_in__lt=payload.check_out, check_out__gt=payload.check_in).exists():
return Status(409, {"message": "Room already booked for these dates"})
booking = Booking.objects.create(**payload.dict())
return Status(201, booking)
@api.delete("/bookings/{booking_id}", response={204: None, codes_4xx: Message})
def cancel_booking(request, booking_id: int):
Booking.objects.filter(id=booking_id).delete()
return Status(204, None)go deeper
Recall that response= declares the output schema and that Status(code, body) chooses the status when several are declared.
Explain the selection order: HttpResponse passthrough, default code, Status override, dict lookup, ConfigError, and output filtering.
Catch the production traps: undeclared codes becoming 500s, HttpError bodies drifting from documented schemas, and HttpResponse returns skipping filtering.
Decide one error-response convention for the whole API - Status with declared schemas or HttpError with a shared shape - so documentation and behaviour cannot diverge.
## What response= declares In **Django Ninja**, the `response=` argument of an operation decorator (`@api.post`, `@router.get` and so on) declares what the operation returns. It serves three purposes at once: - **validation** - the return value is checked against the schema; - **filtering** - only the declared fields are serialised, so a model instance with extra attributes does not leak them; - **documentation** - each code and schema appears in the generated OpenAPI document. There are two forms. A single schema, `response=BookingOut`, is stored as `{200: BookingOut}`. A dict maps codes to schemas: `response={201: BookingOut, 409: Message}`. ## Keys and values in the dict | key or value | meaning | |---|---| | an int key such as `201` | one status code | | a `frozenset` of codes, such as `codes_4xx` from `ninja.responses` | the same schema for every code in the set | | a `frozenset` you build | a custom group, such as `frozenset({409, 423})` | | `...` (Ellipsis) as a key | a catch-all the source accepts for any code not listed | | `None` as a value | an empty body, typically `{204: None}` | The built-in groups are `codes_1xx` to `codes_5xx`, each a `frozenset` of the common codes in that class. ## How the operation picks the schema After the view returns, Ninja's result handling runs these steps: 1. If the result is a Django `HttpResponse` (any `HttpResponseBase`), it is returned untouched: no validation, no filtering. 2. The default code is **200** - or, when the dict has **exactly one** key, that key. So `response={201: BookingOut}` with a plain `return booking` answers 201. 3. If the result is a `ninja.Status`, its `status_code` overrides the default and its `value` becomes the body. A two-element tuple `(code, body)` is still accepted but emits a `DeprecationWarning` in 1.7. 4. The code is looked up in the dict, then the `...` catch-all. If neither matches, Ninja raises `ConfigError("Schema for status 404 is not set in response ...")`. 5. A `None` schema returns an empty response with that code; otherwise the body is validated against the schema and dumped, honouring the decorator's `by_alias`, `exclude_unset`, `exclude_defaults` and `exclude_none` options. That `ConfigError` is raised while the request is being handled, so it reaches the generic exception handler and the client gets a **500** - a programming error, found only on the code path that returns the undeclared status. ## A booking example A create-booking operation typically answers 201 with the booking, 409 when the room is taken and 422 automatically for a bad payload. The 409 needs a schema in the dict; the 422 does not, because request validation is produced by an exception handler, not by the operation's return value. If the view returns a `BookingOut` instance that is already the declared schema class, Ninja skips re-validation and dumps it directly. ## Status versus HttpError There are two ways to answer 409: - `return Status(409, {"message": "Room already booked"})` - the body is validated against the `409` schema and documented in OpenAPI; - `raise HttpError(409, "Room already booked")` from `ninja.errors` - the default handler renders `{"detail": "Room already booked"}`, which bypasses the `response=` dict, so the documented 409 schema and the real body can disagree. Teams usually pick one convention per API so the documentation matches what clients receive. ## Filtering in practice Filtering is the part of `response=` that matters most in review. Suppose the view returns a `Booking` instance and `BookingOut` lists `id`, `room`, `check_in` and `check_out`. The model also has `internal_notes` and `payment_reference`; neither appears in the JSON, because Ninja validates the object **into** the schema and dumps the schema, not the object. Extra keys in a returned dict are ignored the same way. That makes the response schema a security boundary as well as a contract: - a column added to the model stays private until someone adds it to the schema; - a `ModelSchema` with `fields = "__all__"` removes that protection, publishing every new column on the next deploy; - a hand-built `HttpResponse` or `JsonResponse` bypasses the boundary for that code path. ## Common mistakes - Returning `Status(404, ...)` from an operation whose dict lacks 404 and discovering the 500 in production. - Expecting 200 from `response={201: X}` because "200 is always the default". - Declaring `{204: None}` and returning a body - the schema is `None`, so the body is discarded. - Returning `HttpResponse` for convenience and losing validation and filtering on that path.
- Why is the 422 for a bad payload not listed in the response dict?The response dict governs what the view returns. A bad payload never reaches the view: the request-validation exception is rendered by NinjaAPI's ValidationError handler, which builds its own 422 response and does not consult the operation's response mapping.
- What do exclude_none=True and by_alias=True on the decorator change?They are passed to the schema's dump after validation: `exclude_none` drops keys whose value is `None` from the JSON, `by_alias` writes fields under their alias names. They change the output shape, not what the schema accepts.
saying these in an interview costs you the question
- Returning a status code that is not in the response dict just skips validation.
- The default status is always 200 whatever the response dict says.
- Returning a (status, body) tuple is the current recommended form in 1.7.
- The response schema only affects documentation, not the JSON sent.
- A 422 schema must be declared or validation errors are not returned.