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
@actionrequest/response shape if you don’t return a serializer. to_representationoverrides 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 + OrderingFilter — drf-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 generation —
openapi-generatorproduces clients in 30+ languages. - API gateway integration — AWS API Gateway / Kong import OpenAPI directly.
Pitfalls
- Trusting auto-detection blindly. It’s wrong on
@actions andto_representationoverrides; 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
spectacularin CI with prod-like settings. coreapianddrf-yasganddrf-spectacularall in the same project. Pick one. Mixing them wastes setup time and produces conflicting docs.- Auto-generated schema for serializers with
SerializerMethodField— type defaults tostr. 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/. Skipcoreapi(deprecated) anddrf-yasg(OpenAPI 2 only). - “Auto-generation works for
ModelViewSet— when does it break?” —@actions without explicitrequest/responses, plainAPIViewwithout@extend_schema,to_representationreshaping,SerializerMethodField(types asstr), endpoints returning raw dicts. - “How do you keep the schema honest?” —
python manage.py spectacular --fail-on-warnin 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 asecuritySchemescomponent; renders an “Authorize” button in Swagger UI.