skip to content

In Django Ninja, where do the generated OpenAPI schema and the /docs page come from, and how do you configure or hide them?

level: juniorimportance: should knowfreq 40%

answer

  1. built from the code, not written
  2. two URLs under the API mount
  3. Swagger by default, Redoc optional
  4. None switches each one off

basics

~10 s

NinjaAPI builds an OpenAPI 3.1 document from each operation's type hints, schemas, response= and auth=, serves it at openapi.json and renders it at /docs with Swagger UI. docs_url=None hides the page; openapi_url=None hides both.

solid answer

~40 s

Django Ninja generates the schema from the code: paths and methods from the operation decorators, parameters and request bodies from type hints and `Schema` classes, responses from `response=`, and security schemes from `auth=`. `NinjaAPI` serves the document at `openapi_url` (default `/openapi.json`) and an interactive page at `docs_url` (default `/docs`), both under the prefix where `api.urls` is mounted, rendered with Swagger UI or with Redoc via `docs=Redoc()`. Per operation, `summary` defaults to the function name in title case, `description` to its docstring, and `tags`, `operation_id`, `deprecated` and `include_in_schema=False` adjust it. `docs_url=None` hides only the page, `openapi_url=None` removes both, and `docs_decorator=staff_member_required` protects them. `manage.py export_openapi_schema` writes the document to a file for client generation.

code

python · 25 lines
python
from django.contrib.admin.views.decorators import staff_member_required
from django.urls import path
from ninja import NinjaAPI, Redoc

api = NinjaAPI(
    title="Catalogue API",
    version="2.0.0",
    docs=Redoc(),
    docs_decorator=staff_member_required,
)


@api.get("/search", response=list[ProductOut], tags=["search"], operation_id="search_products")
async def search_products(request, q: str):
    """Full-text search over active products."""
    qs = Product.objects.filter(name__icontains=q, is_active=True).select_related("category")
    return [p async for p in qs[:20]]


@api.get("/internal/reindex", include_in_schema=False)
def reindex(request):
    return {"queued": True}


urlpatterns = [path("api/", api.urls)]  # schema at /api/openapi.json, page at /api/docs

go deeper

for a junior

Recall that the schema is generated from type hints and schemas, served at openapi.json, and shown at /docs with Swagger UI.

for a middle

Explain which decorator arguments feed the document, the defaults for summary, description and operationId, and the three ways to hide or protect the docs.

for a senior

Keep the document truthful: declare error responses, give operations stable unique operation IDs, and export the schema in CI so contract changes are reviewed.

for a principal

Decide whether the generated document is the published contract for external consumers, and what process keeps it versioned and backward compatible.

## Where the document comes from **Django Ninja** never asks you to write OpenAPI by hand. When a `NinjaAPI` instance is asked for its schema, it walks every mounted operation and builds an **OpenAPI 3.1.0** document: - **paths and methods** from decorators such as `@api.get("/search")`; - **parameters** (path, query, header, cookie) from the view function's type hints; - **request bodies** from `Schema` parameters, with their JSON Schema placed under components; - **responses** from `response=` - one entry per declared status code; - **security** from `auth=`: each authenticator class contributes a security scheme, so the docs page shows an Authorize button. Because the schema is derived from the same hints Ninja uses to validate requests, the documentation and the runtime behaviour cannot drift apart for anything that is declared. ## The two URLs | `NinjaAPI` argument | default | serves | |---|---|---| | `openapi_url` | `"/openapi.json"` | the JSON document | | `docs_url` | `"/docs"` | the interactive page reading that document | | `docs` | `Swagger()` | the renderer; `Redoc()` is the built-in alternative | | `docs_decorator` | `None` | a decorator wrapped around both views | Both URLs sit under the prefix where `api.urls` is included, so with `path("api/", api.urls)` they are `/api/openapi.json` and `/api/docs`. The docs view can be reversed by name as `openapi-view` inside the API's namespace, which defaults to `api-` plus the API's version, for example `api-1.0.0:openapi-view`. Without `"ninja"` in `INSTALLED_APPS`, the Swagger or Redoc assets load from a CDN; adding it serves them through Django's static files instead, which matters for networks without outside access. ## Shaping each operation The decorator arguments that only affect documentation: - `summary` - defaults to the function name with underscores as spaces, title-cased (`search_products` becomes "Search Products"); - `description` - defaults to the function's docstring; - `tags` - groups operations in the UI; routers can set tags for all their operations; - `operation_id` - defaults to the module path and function name joined with underscores; a duplicate prints a warning, and code generators rely on it being unique; - `deprecated=True` - marks the operation as deprecated; - `include_in_schema=False` - keeps the operation working but out of the document; - `openapi_extra` - merges extra keys into the operation's entry. At API level, `title`, `version`, `description`, `servers` and `openapi_extra` fill the document's info and servers sections. ## Hiding and protecting the docs 1. `NinjaAPI(docs_url=None)` - no page, but `/openapi.json` stays for client generators. 2. `NinjaAPI(openapi_url=None)` - no document and, since the page depends on it, no page. 3. `NinjaAPI(docs_decorator=staff_member_required)` - both stay, visible only to logged-in staff. Public production APIs often keep the document but protect or hide the page; internal APIs often keep everything open to their consumers. ## What a search operation looks like in the docs For an operation declared as `async def search_products(request, q: str, limit: int = 20)` with `response=list[ProductOut]`, the generated page shows: 1. `GET /api/search` under the operation's tag, with the summary "Search Products" and the docstring as its description. 2. Two query parameters: `q`, a required string, and `limit`, an optional integer with default 20. 3. A `200` response whose body is an array of `ProductOut`, with `ProductOut` listed once under the document's components and referenced from the response. 4. A lock icon and an Authorize button if the operation or its router has `auth=`, because the authenticator contributes a security scheme. Whether the view is `async def` or plain `def` makes no difference to the document: it describes the HTTP contract, not how the operation is executed. The page's "Try it out" button sends real requests to the running API, so it is subject to the same authentication as any other client. ## What the document does not show Only declared responses appear. The 422 that request validation produces and the 401 from failed authentication come from exception handlers, not from `response=`, so they are **not listed** unless the operation declares a schema for those codes. Hand-returned `HttpResponse` objects and extra keys a resolver computes are equally invisible unless the schema describes them. ## Exporting the schema `python manage.py export_openapi_schema` prints the document; `--api` takes the dotted import path of a `NinjaAPI` instance (without it, the command looks for one mounted at `/api/`), `--output` writes to a file, and `--indent` and `--sorted` make diffs readable. Committing that file and diffing it in review turns accidental contract changes into visible ones.

  • Why is the 422 validation response missing from a Django Ninja operation's documentation?
    The document lists only the status codes in the operation's `response=`. Request validation errors are produced by NinjaAPI's exception handler, outside that mapping, so they appear only if the operation declares a schema for 422. The same applies to 401 from authentication and to errors raised as `HttpError`.
  • How would you stop an internal operation appearing in the public docs without disabling it?
    Pass `include_in_schema=False` to its decorator. The operation keeps working and keeps its auth, but it is left out of `openapi.json`, so neither the docs page nor generated clients know about it. Hiding is not protection, so it still needs `auth=`.

saying these in an interview costs you the question

  • Django Ninja needs a hand-written YAML file to produce its docs.
  • docs_url=None also removes the openapi.json endpoint.
  • The generated docs list every error the operation can return.
  • include_in_schema=False disables the operation itself.
  • The docs page is only available when DEBUG is True.