drf-spectacular is a OpenAPI Schema generation tool for Django REST Framework (DRF), supporting OpenAPI 3.0 and 3.1.
Its main function is to automatically generate OpenAPI documents based on Serializer, View, URL, Authentication and other information of DRF, and allows developers to supplement content that cannot be automatically inferred through decorators and extension mechanisms.
Typical workflow:
Django Model / Serializer / View
↓
drf-spectacular
↓
OpenAPI Schema
↓
┌────────┴────────┐
↓ ↓
Swagger UI Orval
↓
TypeScript Types
Axios / Query Hooks
1. Basic configuration and automatic inference
Basic configuration
# settings.py
INSTALLED_APPS = [
"rest_framework",
"drf_spectacular",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS":
"drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Example API",
"DESCRIPTION": "Example API Documentation",
"VERSION": "1.0.0",
}
Automatically generate Schema
The most basic case usually requires no additional drf-spectacular code.
from rest_framework import serializers
from rest_framework.generics import RetrieveAPIView
class UserProfileSerializer(serializers.Serializer):
name = serializers.CharField()
avatar = serializers.URLField(allow_null=True)
class UserProfileView(RetrieveAPIView):
serializer_class = UserProfileSerializer
queryset = User.objects.all()
drf-spectacular can automatically infer the response structure based on serializer_class:
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/UserProfile"
Automatically infer main dependencies:
serializer_class/get_serializer_class()querysetget_serializer()- View type and corresponding HTTP operation
- Authentication, paging and filter configuration for DRF
GenericAPIView, RetrieveAPIView, ListAPIView, ModelViewSet are generally easier to automatically infer than bare APIView.
Note: Just calling a Serializer inside APIView.get() does not mean that drf-spectacular can automatically recognize the Serializer.
2. @extend_schema
@extend_schema is the most commonly used decorator for drf-spectacular, used to supplement or override automatically inferred information.
from drf_spectacular.utils import extend_schema
@extend_schema(
request=UsernameUpdateSerializer,
responses={200: UserProfileSerializer},
)
def patch(self, request):
...
Corresponds to:
PATCH Request Body
↓
UsernameUpdateSerializer
HTTP 200 Response
↓
UserProfileSerializer
Common parameters
| parameters | function |
|---|---|
request | Request body Schema |
responses | Response Schema |
parameters | Query, Path, Header, Cookie parameters |
operation_id | OpenAPI operation unique identifier |
summary | Interface short description |
description | Interface detailed description |
tags | Interface grouping |
examples | Request or response example |
auth | Overriding automatically inferred authentication requirements |
deprecated | Mark interface deprecated |
exclude | Exclude interface from Schema |
methods | Limit the HTTP method that the decorator applies to |
versions | Limited applicable API version |
principle: Give priority to automatic inference, and only use @extend_schema for content where the inference is wrong or cannot be inferred.
3. GET Parameters and POST Request Body
GET requests usually use Query Parameters:
@extend_schema(
parameters=[SearchPoiSerializer],
responses=SearchPoiResponseSerializer,
)
def get(self, request):
...
Corresponds to:
GET /api/v1/pois?name=Tokyo&limit=10
POST requests usually use Request Body:
@extend_schema(
request=SearchPoiSerializer,
responses=SearchPoiResponseSerializer,
)
def post(self, request):
...
Corresponds to:
POST /api/v1/pois/search
Content-Type: application/json
{
"name": "Tokyo",
"limit": 10
}
difference
| parameters | OpenAPI object | Main purpose |
|---|---|---|
parameters | Parameter Object | Query、Path、Header、Cookie |
request | Request Body Object | JSON, Form, Multipart and other request bodies |
responses | Responses Object | HTTP response |
Note:
- POST can also have Query Parameters.
- GET Usually Request Body is not used.
parameterscan be passed in eitherOpenApiParameteror Serializer which describes the Query parameters.requestis usually passed in a Serializer, base type, or primitive Schema definition.
4. OpenApiParameter
Used to explicitly describe request parameters.
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiTypes,
)
Query parameters
@extend_schema(
parameters=[
OpenApiParameter(
name="keyword",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=True,
description="Search keyword",
),
OpenApiParameter(
name="limit",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
default=20,
),
]
)
def get(self, request):
...
Corresponds to:
GET /api/v1/search?keyword=tokyo&limit=20
Path parameters
OpenApiParameter(
name="poi_id",
type=OpenApiTypes.INT,
location=OpenApiParameter.PATH,
required=True,
)
Corresponds to:
GET /api/v1/pois/123
The Path parameter must be required.
Header parameters
OpenApiParameter(
name="X-Request-ID",
type=OpenApiTypes.STR,
location=OpenApiParameter.HEADER,
)
Corresponding request:
X-Request-ID: abc-123
Response Header
OpenApiParameter also supports responding to Header via the response parameter description.
OpenApiParameter(
name="Retry-After",
type=OpenApiTypes.INT,
location=OpenApiParameter.HEADER,
response=[429],
description="Retry delay in seconds",
)
Then pass in @extend_schema(parameters=[...]).
What it describes is:
HTTP/1.1 429 Too Many Requests
Retry-After: 35
Instead of the request Header sent by the client.
5. Responses and OpenApiResponse
Define responses based on status codes
@extend_schema(
responses={
200: UserProfileSerializer,
400: ValidationErrorSerializer,
401: AuthenticationErrorSerializer,
429: RateLimitErrorSerializer,
}
)
def get(self, request):
...
Response structures corresponding to different HTTP status codes will be generated.
OpenApiResponse
OpenApiResponse can further add response descriptions and examples.
from drf_spectacular.utils import OpenApiResponse
@extend_schema(
responses={
200: OpenApiResponse(
response=UserProfileSerializer,
description="User profile retrieved",
),
404: OpenApiResponse(
response=ErrorSerializer,
description="User not found",
),
}
)
def get(self, request):
...
For operations without response body:
@extend_schema(
responses={
204: OpenApiResponse(
response=None,
description="Successfully deleted",
)
}
)
def delete(self, request):
...
Multiple media types
@extend_schema(
responses={
(200, "application/json"): UserSerializer,
(200, "application/pdf"): OpenApiTypes.BINARY,
}
)
def get(self, request):
...
Schema can be specified separately for different media types.
In practice, it is not necessary to mechanically repeat all error status codes for each interface, but you should ensure that the document contains important error responses that the client needs to handle. Common errors can be maintained uniformly through public tools or project-level mechanisms.
6. @extend_schema_view
@extend_schema_view is used to collectively describe multiple methods at the class level.
from drf_spectacular.utils import (
extend_schema,
extend_schema_view,
)
@extend_schema_view(
get=extend_schema(
responses=UserProfileSerializer,
),
patch=extend_schema(
request=UserProfileUpdateSerializer,
responses=UserProfileSerializer,
),
delete=extend_schema(
request=None,
responses={204: None},
),
)
class UserProfileView(GenericAPIView):
...
The effect is equivalent to adding @extend_schema to get(), patch(), delete() respectively.
For ViewSet, the Action name is required:
@extend_schema_view(
list=extend_schema(tags=["Users"]),
retrieve=extend_schema(tags=["Users"]),
create=extend_schema(tags=["Users"]),
)
class UserViewSet(ModelViewSet):
...
It is especially suitable for scenarios where DRF Mixin is inherited and there is no explicit override of the operation method.
7. @extend_schema_field
For custom Serializer Field, the return type of drf-spectacular may not be accurately inferred.
For example:
class CoordinatesField(serializers.Field):
def to_representation(self, value):
return [value.longitude, value.latitude]
You can use:
from drf_spectacular.utils import extend_schema_field
@extend_schema_field({
"type": "array",
"items": {"type": "number"},
"minItems": 2,
"maxItems": 2,
})
class CoordinatesField(serializers.Field):
...
Corresponds to:
type: array
items:
type: number
minItems: 2
maxItems: 2
This is more explicit than simply declaring ListField(FloatField()) because it limits the length of the coordinate array.
Note: The above structure represents a length-2 numeric array in OpenAPI, but the TypeScript code generator does not necessarily recognize it as a fixed-length tuple [number, number], and may also generate number[].
8. SerializerMethodField
For SerializerMethodField, it is recommended to use type annotations or @extend_schema_field to make the return type explicit.
class UserSerializer(serializers.Serializer):
display_name = serializers.SerializerMethodField()
@extend_schema_field(serializers.CharField())
def get_display_name(self, obj):
return obj.name.upper()
You can also use the Python type:
@extend_schema_field(str)
def get_display_name(self, obj):
return obj.name.upper()
In the case of simple types, an explicit return type annotation may also be sufficient:
def get_display_name(self, obj) -> str:
return obj.name.upper()
If complex objects, lists, or nested structures are returned, explicit use of the Serializer description is more recommended.
9. @extend_schema_serializer
This is a common decorator worth adding to the original note.
@extend_schema_serializer is used to adjust the Schema performance of the entire Serializer.
For example:
from drf_spectacular.utils import extend_schema_serializer
@extend_schema_serializer(
exclude_fields=("internal_note",),
)
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField()
username = serializers.CharField()
internal_note = serializers.CharField()
internal_note is ignored when generating Schema.
Note: It only affects Schema documents and does not automatically change the data actually output by DRF Serializer.
Envelope structure
Some API are wrapped in unified response:
{
"success": true,
"data": {
"id": 1,
"username": "Alice"
}
}
The response Serializer can be defined explicitly:
class UserResponseSerializer(serializers.Serializer):
success = serializers.BooleanField()
data = UserSerializer()
If you have a large number of unified packaging structures, you can further reuse Schema definitions using extend_schema_serializer, dynamic Serializer factories, or project-level extensions.
10. inline_serializer
For responses that are simple in structure and used only once, it is not necessarily necessary to define a separate Serializer class.
from drf_spectacular.utils import inline_serializer
@extend_schema(
responses=inline_serializer(
name="HealthResponse",
fields={
"status": serializers.CharField(),
"version": serializers.CharField(),
},
)
)
def get(self, request):
...
Corresponds to:
{
"status": "ok",
"version": "1.0.0"
}
Applicable scenarios:
- Health Check。
- Simple status response.
- A one-time small data structure.
If the same structure appears repeatedly in multiple interfaces, it is still recommended to define independent Serializer to facilitate reuse and maintenance.
11. OpenApiExample
OpenApiExample is used to add an example to a request or response.
from drf_spectacular.utils import OpenApiExample
@extend_schema(
request=UsernameUpdateSerializer,
examples=[
OpenApiExample(
name="Username Update",
value={
"username": "alice"
},
request_only=True,
),
],
)
def patch(self, request):
...
Commonly used parameters:
| parameters | function |
|---|---|
name | Example name |
value | Sample data |
summary | short description |
description | Detailed description |
request_only | Only for requests |
response_only | only for response |
media_type | Limited media type |
status_codes | Limit response status code |
For example add example for 200 response:
OpenApiExample(
name="Success",
value={
"id": 1,
"username": "alice",
},
response_only=True,
status_codes=["200"],
)
The example does not automatically verify the data returned during runtime, and is only used for interface documentation and related tool display.
12. File upload and OpenApiRequest
For the file upload interface, multipart/form-data needs to be described.
Serializer
class AvatarUploadSerializer(serializers.Serializer):
avatar = serializers.ImageField()
View
from rest_framework.parsers import MultiPartParser
from drf_spectacular.utils import OpenApiRequest
class AvatarUploadView(GenericAPIView):
parser_classes = [MultiPartParser]
@extend_schema(
request={
"multipart/form-data": OpenApiRequest(
request=AvatarUploadSerializer,
)
},
responses={200: AvatarResponseSerializer},
)
def post(self, request):
...
OpenAPI file fields usually correspond to:
type: string
format: binary
Need to pay attention to:
parser_classesdetermines the request data parsing method actually supported by DRF.@extend_schemais responsible for describing the interface contract.- The Schema definition does not automatically grant View file upload processing capabilities.
For situations where field representations in requests and responses are different, consider:
SPECTACULAR_SETTINGS = {
"COMPONENT_SPLIT_REQUEST": True,
}
This helps more accurately describe the differences between FileField, read-only fields, required fields, etc. in requests and responses, but may increase the number of Schema components generated.
13. Authentication Extension
For custom authentication mechanisms, drf-spectacular may not be automatically recognized.
You can use:
from drf_spectacular.extensions import (
OpenApiAuthenticationExtension,
)
For example:
class BrowserSessionAuthenticationScheme(
OpenApiAuthenticationExtension
):
target_class = (
"myapp.authentication.BrowserSessionAuthentication"
)
name = "browserSession"
def get_security_definition(self, auto_schema):
return {
"type": "apiKey",
"in": "cookie",
"name": "sorivel_session",
}
Corresponding to OpenAPI:
components:
securitySchemes:
browserSession:
type: apiKey
in: cookie
name: sorivel_session
What is defined here is how authentication credentials are passed.
Extension does not automatically implement authentication logic, nor does CSRF protection.
The actual authentication logic is still the responsibility of DRF Authentication Class.
custom certification is generally suitable for using Extension to describe it uniformly at the project level, and there is no need to repeat the statement for each View.
14. Others Extension
drf-spectacular provides a variety of Extension for modifying the automatic inference behavior.
| Extension | function |
|---|---|
OpenApiAuthenticationExtension | Custom authentication mechanism |
OpenApiSerializerFieldExtension | Custom or third party Serializer Field |
OpenApiSerializerExtension | Modify Serializer inference |
OpenApiViewExtension | Modify View inference that cannot be directly controlled |
OpenApiFilterExtension | Custom filter or pagination parameter inference |
Extension is usually used for:
- Third-party libraries.
- Public base classes.
- Decorator code cannot be added directly.
- Custom components that need to be handled uniformly in the project.
Extension Register
The Extension class needs to be imported by Python before Schema is generated.
Can be placed in:
myapp/
├── apps.py
├── schema.py
├── serializers.py
└── views.py
Then import in apps.py:
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.schema # noqa: F401
In this way, when drf-spectacular generates Schema, the corresponding Extension can be found.
15. Postprocessing Hook
Postprocessing Hook is used to uniformly modify the results after OpenAPI Schema is completely generated.
def postprocess_contract(
result,
generator,
request,
public,
):
# Modify OpenAPI Schema
return result
Configuration:
SPECTACULAR_SETTINGS = {
"POSTPROCESSING_HOOKS": [
"drf_spectacular.hooks.postprocess_schema_enums",
"common.api.schema.postprocess_contract",
],
}
Note: Resetting POSTPROCESSING_HOOKS will overwrite the default configuration.
If you need to keep the default Enum handling, you should explicitly add:
"drf_spectacular.hooks.postprocess_schema_enums"
Typical uses
- Modify the generated
operationId. - Supplement unified metadata for some Schema.
- Adjust component naming or references.
- Modify the general response structure.
- Handle special Schema that the code generator is not compatible with.
Preprocessing Hook
Unlike Postprocessing, Preprocessing Hook is executed before Schema is generated and is mainly used to process the interface list to be generated.
For example:
SPECTACULAR_SETTINGS = {
"PREPROCESSING_HOOKS": [
"drf_spectacular.hooks.preprocess_exclude_path_format",
],
}
The built-in Hook can remove duplicate paths produced by DRF format suffix.
principle: Serializer can be expressed first, use Serializer; extend_schema can be expressed, extend_schema should be used first; Hook mainly handles global Schema conversion.
16. Custom AutoSchema
drf-spectacular uses by default:
drf_spectacular.openapi.AutoSchema
The underlying inference behavior can be changed through inheritance:
from drf_spectacular.openapi import AutoSchema
class ApiAutoSchema(AutoSchema):
...
Common uses include:
- Custom Operation ID generation.
- Modify parameter inference.
- Modify response structure inference.
- Inferences to Modify Certification Requirements.
For example covering Operation ID:
class ApiAutoSchema(AutoSchema):
def get_operation_id(self):
operation_id = super().get_operation_id()
return operation_id
In actual projects, you should try to avoid overwriting internal inference methods at will, because such customization is strongly coupled with the implementation details of drf-spectacular, and compatibility needs to be re-verified when upgrading the version.
17. operationId
Each OpenAPI Operation should have a unique operationId.
For example:
operationId: getUserProfile
Code generation tools such as Orval typically generate the API function or Hook based on it:
getUserProfile();
useGetUserProfile();
The exact name depends on the code generation configuration.
Manually specify
@extend_schema(
operation_id="getUserProfile",
)
def get(self, request):
...
Automatically generated
drf-spectacular can automatically generate Operation ID based on information such as URL and HTTP methods.
For example:
/api/v1/users/profile/
↓
users_profile_retrieve
Whether manual specification is required depends on project requirements:
- Internal API, unified naming rules: automatic generation first.
- Released frontend SDK: It is recommended to maintain Operation ID for long-term stability.
- Automatically generated names are unclear or conflicting: they can be specified manually.
For Orval, operationId changes may cause the generated function name to change, thereby affecting the front-end code.
18. Tags
Tags is used for interface grouping:
@extend_schema(
tags=["Users"],
)
def get(self, request):
...
Swagger UI and ReDoc are usually grouped according to the Tags display interface.
Orval:
mode: "tags-split";
The resulting API file can also be split based on Tags.
Therefore, Tags not only affects document display, but may also affect the directory structure of the front-end generated code.
It is recommended that a business module adopt a unified naming of Tag, for example:
Users
POI
Itinerary
Authentication
19. Commonly used SPECTACULAR_SETTINGS
| Configuration | function |
|---|---|
TITLE | API title |
DESCRIPTION | API Description |
VERSION | API version |
SERVERS | API server address |
TAGS | Top-level Tag definition |
COMPONENT_SPLIT_REQUEST | Separating requests and responses Schema |
COMPONENT_NO_READ_ONLY_REQUIRED | Adjust required behavior for read-only fields |
ENUM_NAME_OVERRIDES | Custom enumeration component name |
POSTPROCESSING_HOOKS | Processing after Schema generation |
PREPROCESSING_HOOKS | Schema Processing before generation |
SORT_OPERATIONS | Interface sorting |
SCHEMA_PATH_PREFIX | Schema path prefix identification |
OAS_VERSION | OpenAPI standard version |
Most projects do not require modification of all configurations.
It is recommended to keep the default configuration first and only adjust it for actual problems.
20. Schema generation and CI verification
Generate Schema
python manage.py spectacular \
--file openapi.yaml \
--validate \
--fail-on-warn
Parameters:
--file: Output file.--validate: Verify the generated OpenAPI Schema.--fail-on-warn: Causes the command to fail when the Schema generation warning appears.
Need to distinguish:
- Schema verification: Verify whether the document structure conforms to the OpenAPI specification.
- API Test: Verify that the real interface works as expected.
- TypeScript Typecheck: Verify whether the type of front-end calling code is correct.
These three are not substitutes for each other.
CI/CD process
Modify Django API
↓
pytest
↓
spectacular --validate
↓
generate openapi.yaml
↓
Orval
↓
TypeScript Typecheck
↓
Build / Deploy
Find out early with CI:
- Schema cannot be generated correctly.
- Serializer field changes.
- Operation ID changes.
- The generated TypeScript type is incompatible with existing code.
- API Client generation failed.
But if the front and back ends are located in different warehouses, you need to know how to synchronize Schema and in which repository to execute Orval.
21. Frequently Asked Questions and Practical Principles
Why does Schema generate the wrong type?
Common reasons:
- Custom Serializer Field cannot be automatically inferred.
SerializerMethodFieldType information is missing.- View does not declare Serializer.
- The actual Response structure is inconsistent with Serializer.
- The custom authentication or paging mechanism is not recognized.
Priority should be given to supplementing the corresponding Serializer, View or Field with accurate information rather than directly modifying the generated YAML.
Do I need to write extend_schema for each interface?
unnecessary.
If the definition of DRF itself is clear enough, drf-spectacular can automatically generate a large amount of Schema information.
Recommended extension priority
DRF natural definition
↓
Serializer / GenericAPIView
↓
@extend_schema
↓
@extend_schema_field
@extend_schema_serializer
↓
Extension
↓
Pre/Postprocessing Hook
↓
Customize AutoSchema
This is an engineering choice sequence, not a framework mandate.
The farther back, usually means greater customization capabilities, and may also increase maintenance costs.
Summary
The important functions of drf-spectacular can be divided into four categories:
| Classification | core content |
|---|---|
| Automatically generated | Serializer、View、queryset、AutoSchema |
| Interface customization | extend_schema、extend_schema_view、OpenApiParameter、OpenApiResponse |
| Schema customized | extend_schema_field、extend_schema_serializer、inline_serializer、OpenApiExample |
| Advanced extensions | Extensions, Hooks, custom AutoSchema |
For ordinary Django REST Framework projects, you most need to be familiar with:
serializer_classwith automatic inference.@extend_schemaand@extend_schema_view.parameters、request、responses。@extend_schema_field。operation_idand Tags.- Schema verification and client code generation.
Consider Extension, Hook, and custom AutoSchema again when your project contains custom authentication, complex response wrappers, third-party Serializer Field, or unified Schema conversion requirements.