Spectacular¶
RestflowAutoSchema is a drop-in replacement for drf-spectacular's
AutoSchema that understands restflow's view conventions, including
per-action serializer splits, request/response serializer pairs,
and pagination resolution through ActionConfig.
Features¶
drf-spectacular generates an OpenAPI 3.x schema from a DRF project,
along with Swagger UI and Redoc views to browse the schema in the
browser. The default AutoSchema reads a single serializer_class
attribute on the view, which does not fit restflow viewsets that
declare per-action overrides through action_configs or split
request and response shapes through request_serializer_class and
response_serializer_class.
RestflowAutoSchema adds:
- Awareness of
action_configs[<action>]so the schema reflects the serializer that will actually run for the current action. - A separate request/response serializer resolution path that picks
up
get_request_serializer_class()andget_response_serializer_class()defined by theAPIViewHelpersMixinandAsyncViewSetMixin. - Pagination resolution from
ActionConfig.pagination_classandview.get_pagination_class(), so list endpoints render the correct paginated envelope. - Automatic
many=Truehandling on GET list endpoints when a pagination class is resolved and the URL is not a detail route.
Install and setup¶
Install drf-spectacular through the optional extra:
Wire the schema class into the DRF settings:
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"drf_spectacular",
"restflow.caching",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "restflow.spectacular.RestflowAutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Example API",
"DESCRIPTION": "An API powered by Restflow.",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
}
Mount the schema endpoint and one or both UI views:
# urls.py
from django.urls import path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
SpectacularRedocView,
)
urlpatterns = [
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/schema/swagger/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
path(
"api/schema/redoc/",
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
]
Importing restflow.spectacular raises ImportError if drf-spectacular
is not installed. The error message points at the install command above.
Request serializer resolution¶
When the schema generator asks for the request serializer of an
operation, RestflowAutoSchema walks the following lookup chain
and returns the first non-None match:
view.action_configs[view.action].request_serializer_classif an action config is registered for the current action and the field is set.view.action_configs[view.action].serializer_classfor the same action.view.get_request_serializer_class()if the method is callable.- drf-spectacular's default lookup, which reads
view.serializer_class.
If a step raises AttributeError, ImproperlyConfigured, or TypeError
during schema generation, the failure is logged and the chain falls
through to the next step. Errors during request handling itself are
not affected by this safety net.
Response serializer resolution¶
The response serializer lookup mirrors the request side:
view.action_configs[view.action].response_serializer_class.view.action_configs[view.action].serializer_class.view.get_response_serializer_class()if callable.view.serializer_classwhenmany=Trueis needed for a paginated list endpoint and no earlier step matched.- drf-spectacular's default lookup.
Steps 1 through 4 wrap the serializer in many=True automatically
for paginated list responses. The detection rules for many=True
are described in the next section.
List vs detail detection¶
many=True is applied when all of the following hold:
- The HTTP method is GET.
- The view does not look like a detail route. The schema checks the
resolved
lookup_url_kwargorlookup_fieldagainst the path and the path regex. If the kwarg appears in either form, the operation is treated as a detail endpoint andmany=Trueis not applied. - A pagination class is resolved.
Pagination resolution itself walks:
view.action_configs[view.action].pagination_class.view.get_pagination_class().view.pagination_class.
If no pagination class resolves, the schema does not assume a list shape even on GET, so single-item GET endpoints without an ID kwarg still render as the single-object response.
Request and response serializer split¶
Restflow encourages a request/response serializer split: a write shape used to validate the incoming body, and a read shape used to serialise the response. The schema renders both correctly without extra annotations.
from restflow.views import AsyncModelViewSet
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
request_serializer_class = ProductWriteSerializer
response_serializer_class = ProductReadSerializer
POST, PUT, and PATCH request bodies render with
ProductWriteSerializer. 2xx responses on every action render with
ProductReadSerializer. List endpoints wrap the read serializer in
the paginator envelope when a pagination class is in effect.
Per-action overrides are declared with ActionConfig:
from restflow.views import AsyncModelViewSet, ActionConfig
from restflow.pagination import FastPageNumberPagination
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductReadSerializer
request_serializer_class = ProductWriteSerializer
action_configs = {
"list": ActionConfig(
response_serializer_class=ProductListSerializer,
pagination_class=FastPageNumberPagination,
),
"create": ActionConfig(
request_serializer_class=ProductCreateSerializer,
),
}
The list operation in the schema uses ProductListSerializer as the
item shape and the FastPageNumberPagination envelope. The create
operation uses ProductCreateSerializer for the request body and
ProductReadSerializer for the response.
Pagination in the schema¶
Paginated list endpoints render with the wrapped envelope that matches the pagination class:
PageNumberPaginationandLimitOffsetPaginationproduce the standard envelope withcount,next,previous, andresults.FastPageNumberPaginationomits thecountfield. The schema reflects that omission, so consumers do not see a property that the API never returns.CursorPaginationproduces the cursor envelope withnext,previous, andresults, plus the cursor query parameters.
Detail endpoints, action endpoints, and any GET that does not resolve a pagination class render the bare object shape without an envelope.
Filter backend integration¶
RestflowFilterBackend.get_schema_operation_parameters() is invoked
by drf-spectacular automatically. Every declared FilterSet field
becomes a query parameter in the OpenAPI schema, including:
- The base field, with the type and constraints derived from the field declaration.
- Lookup variants such as
price__gte,name__icontains, and the full set generated bylookups=...or a lookup category. - Negation variants with the
!suffix. - Ordering parameters when
OrderFieldorMeta.order_fieldsis declared.
from rest_framework import generics
from restflow.filters import (
FilterSet, RestflowFilterBackend, StringField, IntegerField,
)
class ProductFilterSet(FilterSet):
name = StringField(lookups=["icontains"])
price = IntegerField(lookups=["comparison"])
class ProductListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [RestflowFilterBackend]
filterset_class = ProductFilterSet
The list endpoint surfaces name, name__icontains, name!,
name__icontains!, price, price__gt, price__gte, price__lt,
price__lte, and the corresponding negation variants in the
generated parameters list.
Custom action endpoints¶
drf-spectacular's @action decorator works without changes. The
schema follows the regular resolution chain, so the action's own
serializer_class (if provided) wins through the standard
mechanism.
from rest_framework.decorators import action
from restflow.views import AsyncModelViewSet
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductReadSerializer
@action(detail=False, methods=["post"], serializer_class=ImportPayloadSerializer)
async def bulk_import(self, request):
...
For per-action serializer or pagination overrides without an
@action decorator, declare an entry in action_configs keyed by
the action name. The schema picks it up through the same lookup
chain used at runtime.
Plain APIView support¶
For views that are not generic viewsets, set the relevant attributes
on the class. The helpers mixin under restflow's APIView and
AsyncAPIView already exposes get_request_serializer_class and
get_response_serializer_class, so the schema picks them up
automatically.
from restflow.views import AsyncAPIView
class TokenRefreshView(AsyncAPIView):
request_serializer_class = TokenRefreshRequestSerializer
response_serializer_class = TokenResponseSerializer
async def post(self, request):
ser = await self.avalidated_serializer()
result = await refresh_token(ser.validated_data)
return await self.aserialized_response(result)
The POST body in the schema renders with
TokenRefreshRequestSerializer and the 2xx response renders with
TokenResponseSerializer. No extra schema annotations are required.
filterset_class also works on plain APIView and AsyncAPIView.
The filter query parameters appear in the schema automatically.
from restflow.views import AsyncAPIView
from restflow.filters import FilterSet, StringField
class ProductFilterSet(FilterSet):
name = StringField(lookups=["icontains"])
class ProductSearchView(AsyncAPIView):
serializer_class = ProductSerializer
filterset_class = ProductFilterSet
async def get(self, request):
qs = ProductFilterSet(request=request).filter_queryset(
Product.objects.all()
)
return await self.aserialized_response(qs)
The name and name__icontains parameters appear in the schema
without any additional annotations.
Customising further¶
Two extension points cover most cases:
- Subclass
RestflowAutoSchemaand override the resolution hooks for project-specific behaviour. The hooks areget_request_serializer,get_response_serializers,restflow_action_config,restflow_serializer_class, andresolved_pagination_class. - Use drf-spectacular's
extend_schemadecorator for inline overrides on a single operation.extend_schemawins over the resolution chain, which makes it the right tool for one-off cases such as documenting an operation that returns a non-serializer shape.
Common pitfalls¶
- Forgetting to install the extra.
from restflow.spectacular import RestflowAutoSchemaraisesImportErrorwith a message that names the install command. Install Restflow with the spectacular extra to fix it. - Setting
DEFAULT_SCHEMA_CLASSto a string path that does not match. DRF silently falls back to the defaultAutoSchema, which hides every restflow-specific resolution rule. Double-check the dotted path. - Action configs that override
serializer_classbut notresponse_serializer_classexplicitly. The response uses the sharedserializer_class, which is fine when read and write share a shape but wrong when the read shape differs. Setresponse_serializer_classon theActionConfigto be explicit. - Mixing
@extend_schema(responses=...)andActionConfigfor the same action.extend_schemawins, so anActionConfigchange goes unreflected in the schema. Pick one approach per operation.