backend / web frameworks / django / drf / 16_schema_openapi.md

DRF Schema and OpenAPI

5 interview angles 4 min read source

DRF Schema and OpenAPI

Schemas describe your API in a machine-readable format (OpenAPI 3 / Swagger). They power: client SDK generation, interactive docs (Swagger UI / Redoc / Stoplight), contract testing, and frontend type generation.

DRF has built-in schema generation, but it’s limited. The standard for production is drf-spectacular.

The landscape

Option Status Use
coreapi (built into DRF) Deprecated as of DRF 3.10+ Don’t use for new work
Built-in AutoSchema (OpenAPI 3) Maintained but minimal Quick docs, no fine control
drf-yasg Mature, OpenAPI 2 (Swagger) — slower release pace Legacy projects
drf-spectacular OpenAPI 3, actively maintained Default choice for new DRF projects

The rest of this file uses drf-spectacular.

Setup

pip install drf-spectacular
# settings.py
INSTALLED_APPS += ["drf_spectacular"]

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

SPECTACULAR_SETTINGS = {
    "TITLE": "My API",
    "DESCRIPTION": "...",
    "VERSION": "1.0.0",
    "SERVE_INCLUDE_SCHEMA": False,   # don't expose the raw schema endpoint inside docs
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

urlpatterns += [
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema")),
    path("api/redoc/", SpectacularRedocView.as_view(url_name="schema")),
]

Now /api/schema/ returns OpenAPI YAML/JSON, /api/docs/ is interactive Swagger UI, /api/redoc/ is Redoc.

What it picks up automatically

For a ModelViewSet:

  • All standard CRUD operations with their HTTP methods, paths, path parameters.
  • Request/response schemas from serializer_class.
  • Field types, required/optional, defaults, descriptions from serializer fields’ help_text.
  • Authentication classes from authentication_classes.
  • Pagination wrapper from the configured pagination class.

What it can’t infer:

  • Custom @action request/response shape if you don’t return a serializer.
  • to_representation overrides that change the shape.
  • Conditional response based on permissions/version.
  • Examples.

Decorating views for accuracy

from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample, OpenApiResponse

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    @extend_schema(
        summary="Publish a book",
        description="Marks the book as published and emits a `book.published` event.",
        request=PublishRequestSerializer,
        responses={
            200: BookSerializer,
            409: OpenApiResponse(description="Already published"),
        },
        examples=[OpenApiExample("Default", value={"send_notification": True})],
    )
    @action(detail=True, methods=["post"])
    def publish(self, request, pk=None):
        ...

@extend_schema is the main lever. Use it on:

  • Every @action (the auto-detection often guesses wrong).
  • Endpoints whose response shape isn’t a serializer (raw dicts, file downloads).
  • Endpoints with multiple possible status codes you want documented (404, 409, 422).

Documenting query params

For viewset filters that aren’t picked up:

@extend_schema(
    parameters=[
        OpenApiParameter("q", str, description="Search query"),
        OpenApiParameter("category", int, OpenApiParameter.QUERY, required=False),
    ],
)
def list(self, request, *args, **kwargs):
    ...

Often unnecessary if you use DjangoFilterBackend + SearchFilter + OrderingFilterdrf-spectacular introspects them.

Multiple serializers per action

@extend_schema_view(
    list=extend_schema(responses={200: BookListSerializer(many=True)}),
    create=extend_schema(request=BookCreateSerializer, responses={201: BookDetailSerializer}),
    retrieve=extend_schema(responses={200: BookDetailSerializer}),
)
class BookViewSet(viewsets.ModelViewSet):
    ...

extend_schema_view wraps an entire viewset; extend_schema decorates a single method.

Generating a static schema (for CI / SDK gen)

python manage.py spectacular --file schema.yml --validate

--validate runs OpenAPI 3 validation. Check the file into git or publish as a build artifact. Frontend teams can run openapi-typescript schema.yml -o api.ts to get typed clients.

Auth in the schema

SPECTACULAR_SETTINGS["SECURITY"] = [{"jwtAuth": []}]
SPECTACULAR_SETTINGS["COMPONENTS"] = {
    "securitySchemes": {
        "jwtAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
    },
}

This adds the “Authorize” button in Swagger UI so testers can paste a JWT.

Common warnings and how to fix them

drf-spectacular prints warnings on schema generation. Common ones:

Warning Fix
unable to guess serializer for an @action Add @extend_schema(request=..., responses=...)
enum field has no choices Use serializers.ChoiceField with explicit choices
unhandled lookup_field type for non-int pks Set lookup_field and OpenApiParameter accordingly
view is not a subclass of GenericAPIView Add @extend_schema per method on plain APIView

Run python manage.py spectacular --fail-on-warn in CI to block warning-introducing PRs.

Versioning + schemas

path("api/v1/schema/", SpectacularAPIView.as_view(api_version="v1")),
path("api/v2/schema/", SpectacularAPIView.as_view(api_version="v2")),

See 15_versioning.md.

Why bother with a schema at all?

  • Frontend types — generate TypeScript interfaces; refactors break compile, not runtime.
  • Contract tests — Schemathesis / Dredd hammer your API against the schema and catch drift.
  • Docs — Swagger/Redoc are free, always-up-to-date, interactive (try-it-out works against the live API).
  • SDK generationopenapi-generator produces clients in 30+ languages.
  • API gateway integration — AWS API Gateway / Kong import OpenAPI directly.

Pitfalls

  • Trusting auto-detection blindly. It’s wrong on @actions and to_representation overrides; always spot-check the rendered schema.
  • Generating schema in dev only. If schema generation throws warnings/errors only in prod settings (e.g. due to env-conditional URL config), schema CI fails surprisingly. Run spectacular in CI with prod-like settings.
  • coreapi and drf-yasg and drf-spectacular all in the same project. Pick one. Mixing them wastes setup time and produces conflicting docs.
  • Auto-generated schema for serializers with SerializerMethodField — type defaults to str. Use @extend_schema_field(int) to fix.

Interview angle

  • “How would you generate API docs for a DRF project today?”drf-spectacular, expose /api/schema/ + /api/docs/. Skip coreapi (deprecated) and drf-yasg (OpenAPI 2 only).
  • “Auto-generation works for ModelViewSet — when does it break?”@actions without explicit request/responses, plain APIView without @extend_schema, to_representation reshaping, SerializerMethodField (types as str), endpoints returning raw dicts.
  • “How do you keep the schema honest?”python manage.py spectacular --fail-on-warn in CI; contract testing with Schemathesis; check the schema file into git so PRs reveal API surface changes.
  • “Why generate a schema at all?” — typed frontend clients, contract testing, always-up-to-date docs, SDK generation, API gateway import.
  • “How do you document JWT/OAuth in the schema?”SPECTACULAR_SETTINGS["SECURITY"] and a securitySchemes component; renders an “Authorize” button in Swagger UI.