← ProgrammingBack to top

drf-spectacular

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()
  • queryset
  • get_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

parametersfunction
requestRequest body Schema
responsesResponse Schema
parametersQuery, Path, Header, Cookie parameters
operation_idOpenAPI operation unique identifier
summaryInterface short description
descriptionInterface detailed description
tagsInterface grouping
examplesRequest or response example
authOverriding automatically inferred authentication requirements
deprecatedMark interface deprecated
excludeExclude interface from Schema
methodsLimit the HTTP method that the decorator applies to
versionsLimited 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

parametersOpenAPI objectMain purpose
parametersParameter ObjectQuery、Path、Header、Cookie
requestRequest Body ObjectJSON, Form, Multipart and other request bodies
responsesResponses ObjectHTTP response

Note:

  • POST can also have Query Parameters.
  • GET Usually Request Body is not used.
  • parameters can be passed in either OpenApiParameter or Serializer which describes the Query parameters.
  • request is 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:

parametersfunction
nameExample name
valueSample data
summaryshort description
descriptionDetailed description
request_onlyOnly for requests
response_onlyonly for response
media_typeLimited media type
status_codesLimit 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_classes determines the request data parsing method actually supported by DRF.
  • @extend_schema is 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.

Extensionfunction
OpenApiAuthenticationExtensionCustom authentication mechanism
OpenApiSerializerFieldExtensionCustom or third party Serializer Field
OpenApiSerializerExtensionModify Serializer inference
OpenApiViewExtensionModify View inference that cannot be directly controlled
OpenApiFilterExtensionCustom 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

Configurationfunction
TITLEAPI title
DESCRIPTIONAPI Description
VERSIONAPI version
SERVERSAPI server address
TAGSTop-level Tag definition
COMPONENT_SPLIT_REQUESTSeparating requests and responses Schema
COMPONENT_NO_READ_ONLY_REQUIREDAdjust required behavior for read-only fields
ENUM_NAME_OVERRIDESCustom enumeration component name
POSTPROCESSING_HOOKSProcessing after Schema generation
PREPROCESSING_HOOKSSchema Processing before generation
SORT_OPERATIONSInterface sorting
SCHEMA_PATH_PREFIXSchema path prefix identification
OAS_VERSIONOpenAPI 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.
  • SerializerMethodField Type 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.

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:

Classificationcore content
Automatically generatedSerializer、View、queryset、AutoSchema
Interface customizationextend_schema、extend_schema_view、OpenApiParameter、OpenApiResponse
Schema customizedextend_schema_field、extend_schema_serializer、inline_serializer、OpenApiExample
Advanced extensionsExtensions, Hooks, custom AutoSchema

For ordinary Django REST Framework projects, you most need to be familiar with:

  1. serializer_class with automatic inference.
  2. @extend_schema and @extend_schema_view.
  3. parameters、request、responses。
  4. @extend_schema_field。
  5. operation_id and Tags.
  6. 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.


Reference documentation