Skip to content

OpenAPI Generation

The OpenAPI spec is generated by drf-spectacular from the views and their Pydantic schemas, and nobody edits it by hand. What the generator cannot infer, the view states in @extend_schema.

Every ViewSet action is decorated, with summary, description, parameters and responses.

Use simple resource names as tags. A module only names its tags; grouping them by module happens when the published specs are generated (Configuration).

Admin tags: plain resource name (admin is the default audience):

Products # not "PIM Admin: Products"
Categories
FAQ Groups
Delivery Points

Public tags: prefix with Public to distinguish from admin:

Public Delivery Points # not "Delivery Points" (that's admin)
Public Delivery Point Types
Public FAQ

Apply via @extend_schema_view on the ViewSet class:

from drf_spectacular.utils import extend_schema, extend_schema_view
# Admin ViewSet
@extend_schema_view(
list=extend_schema(tags=["Products"]),
retrieve=extend_schema(tags=["Products"]),
)
class ProductViewSet(viewsets.ViewSet): ...
# Public ViewSet
@extend_schema_view(
list=extend_schema(tags=["Public Delivery Points"]),
retrieve=extend_schema(tags=["Public Delivery Points"]),
)
class PublicPointViewSet(viewsets.ViewSet): ...

The summary in @extend_schema is the page title and sidebar label in the published docs. Every action has one, written for a person.

Naming convention:

  • List: “List {resources}” (e.g. “List products”)
  • Retrieve: “Retrieve {resource} by {identifier}” (e.g. “Retrieve product by SKU”)
  • Create/Update/Delete: “{Verb} {resource}”
  • Custom actions: descriptive verb phrase (e.g. “List features in set”)

Do NOT set operation_id manually. Auto-generated IDs are predictable and consistent. A list endpoint gets its _list id when it is a ViewSet list action with a response schema: the item model when the view sets pagination_class (the paginator documents the envelope), an explicit list model otherwise.

After any ViewSet change:

  1. make schema: regenerate the spec
  2. Check Swagger UI (/api/schema/swagger-ui/): no _2 suffixes, all operations have clean summaries
  3. The docs portal build must pass with the regenerated spec
from drf_spectacular.utils import extend_schema, OpenApiParameter
class ProductViewSet(viewsets.ViewSet):
@extend_schema(
summary="List products",
description="Returns paginated product catalog for the given channel.",
parameters=[
OpenApiParameter(name="channel_idx", location="path", description="Channel identifier"),
OpenApiParameter(name="search", description="Search by SKU or name"),
],
responses={200: ProductListResponse},
)
def list(self, request, *args, **kwargs): ...

In the service:

Terminal window
make schema # uv run python manage.py spectacular --file schema.yaml
uv run python manage.py spectacular --validate --fail-on-warn # validate

A module runs the same validation as a test (pattern: tests/test_package.py of entirius-django-siteintel):

def test_openapi_schema_validates(tmp_path):
call_command("spectacular", "--validate", "--fail-on-warn", "--file", str(tmp_path / "schema.yaml"))

The service serves the spec (main/urls.py); the host is http://localhost:8000 under make run, :8100 in the harness:

URLWhat
/api/schema/swagger-ui/Swagger UI
/api/schema/redoc/Redoc
/api/schema/raw schema

The three routes are staff-only by default (API_SCHEMA_PUBLIC = False in the service settings); the harness opens them.

In the service settings (main/settings.py):

SPECTACULAR_SETTINGS = {
"TITLE": "Entirius Volkanos Service API",
"DESCRIPTION": "Base Volkanos service of the Entirius platform",
"VERSION": _PROJECT_VERSION,
"SERVE_INCLUDE_SCHEMA": False,
"OAS_VERSION": "3.1.0",
}

OAS_VERSION is 3.1.0 because modules describe their Pydantic schemas with examples (JSON Schema 2020-12), which is only legal from OpenAPI 3.1 on; under the 3.0.3 default spectacular --validate fails on every module that documents an example.

The published specs are generated by the harness: make schema in entirius-zeno applies the audience split (admin and storefront), the v2 filter and the tag groups with their descriptions. The service and the modules carry no hook for the split, the filter or the groups; a module only names its tags.

Build a Module: the standard applied to one module, from an empty folder to a green schema validation