Migrating from DRF¶
aiodrf subclasses DRF and keeps its names, so a project moves one endpoint at a time. The implementation guide says what happens to each kind of code; this guide describes migration. Shared configuration and execution boundaries are documented in Using aiodrf with DRF.
1. Before moving an endpoint¶
Record what the endpoint does now: bodies, status codes and headers for valid and invalid requests, authentication and permission behaviour, the schema, the query count. Those are the acceptance criteria afterwards.
List the hooks the view and its serializers override (initial,
get_queryset, get_permissions, perform_create, finalize_response,
filters, fields, validators, renderers, the exception handler). Synchronous
DRF hooks retain their contracts through worker adaptation. Verify custom
composition and thread-local assumptions: running a hook in a worker thread
does not guarantee that every extension behaves as before.
The inventory also identifies hooks that could use awaited I/O.
No database change is needed. Decide the transaction boundary first if the
view relies on ATOMIC_REQUESTS (section 5).
2. Imports¶
from rest_framework import serializers
from django_filters.rest_framework import DjangoFilterBackend
from aiodrf import viewsets
class BookSerializer(serializers.ModelSerializer): # unchanged
class Meta:
model = Book
fields = ["id", "title"]
class BookViewSet(viewsets.ModelViewSet): # the only changed import
queryset = Book.objects.all()
serializer_class = BookSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ["title"]
aiodrf's generic views drive plain DRF serializers. Filter backends,
paginators, authentication and permission classes stay the ones configured
in REST_FRAMEWORK; routers and action names are DRF's. Add "aiodrf" to
INSTALLED_APPS for the system checks and the drf-spectacular extensions.
python -m aiodrf.codemod rewrites the imports (section 6).
3. Settings¶
The defaults are the conservative choices:
AIODRF = {
"VALIDATION_UNKNOWN": "thread", # validation code aiodrf cannot classify
"REPRESENTATION_MODE": "thread", # serializer.data asked for from async code
"SERIALIZER_BACKEND": "drf",
}
"inline" for the first two is the project's assertion that the code never
blocks, or that the instances are loaded; a query made there raises
SynchronousOnlyOperation and nothing is repeated in a thread.
"optimistic" is not supported: it raises ImproperlyConfigured, and
manage.py check reports it as aiodrf.E001. Do not set
DJANGO_ALLOW_ASYNC_UNSAFE to hide an error.
4. Async application code¶
from aiodrf import serializers
from aiodrf.response import Response
from aiodrf.views import APIView
class MessageSerializer(serializers.Serializer):
text = serializers.CharField(max_length=200)
class MessageView(APIView):
async def post(self, request):
serializer = MessageSerializer(data=await request.adata())
await serializer.ais_valid(raise_exception=True)
return Response(await serializer.adata())
- aiodrf serializers have
ais_valid(),asave()andadata(). For a plain DRF serializer useawait aio.is_valid(serializer),aio.save,aio.datafromaiodrf.aio. - In a hand-written async handler read the body with
await request.adata();request.datais still DRF's synchronous property. The defaultaperform_authenticationhas loadedrequest.userbefore the handler runs. A view that overridesperform_authenticationto authenticate lazily callsawait request.auser()where it needs the user. Readingrequest.useron the event loop cannot run the authenticators, so aiodrf still authenticates before a permission or throttle that runs there and may read the user: DRF's rate throttles, and any class with async members. DRF'sAllowAny, and synchronous policies run in a worker, stay lazy. - Every hook is a sync/async pair:
get_queryset/aget_queryset,get_object/aget_object,perform_create/aperform_create,validate/avalidate,validate_<field>/avalidate_<field>,create/acreate,to_representation/ato_representation. Implement either member. The other one bridges to it, so a synchronous caller (the browsable API, schema generation, a legacyinitial()) reaches an async-only hook and an async caller reaches a synchronous one. Inside an async member, call the async member ofsuper()(await super().ato_representation(instance)): the synchronous one, called on the event loop, raisesRuntimeError. The bridges are aiodrf's serializers': a plain DRF serializer's async members run only where aiodrf calls them (its generic views,aiodrf.aio). An aiodrf view'sperform_create()orperform_update()written for DRF raisesImproperlyConfiguredfor such a serializer instead of skippingacreate(); DRF's ownis_valid(),save()and.data, called by a DRF view or by a handler of the project's, leave the coroutines unawaited (system checkaiodrf.W010reports the DRF views). Inherit fromaiodrf.serializers, or write the async member of the caller (aperform_create). - Async policies subclass
aiodrf.permissions.BasePermission(ahas_permission,ahas_object_permission),aiodrf.authentication.BaseAuthentication(aauthenticate) oraiodrf.throttling.BaseThrottle(aallow_request). - Async validators and hooks are awaited in DRF's order, and synchronous work is never moved across one to save a hop: two relation fields with an async validator between their lookups cost two hops.
Synchronous code that does no I/O can be declared, and then runs on the event loop instead of a thread:
from rest_framework.permissions import BasePermission
from aiodrf.utils import async_safe
@async_safe
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner_id == request.user.pk
The declaration covers what the class defines. A subclass that adds methods has to be declared itself.
5. Transactions¶
Django refuses ATOMIC_REQUESTS for async views (aiodrf.W002 reports the
setting at start-up). aiodrf has ATOMIC_SAVE instead, on by default: the
default synchronous save, create() or update() with their many-to-many
writes, runs in transaction.atomic() on the database the router selects. It
does not cover perform_create as a whole, an acreate/aupdate, a
serializer's own save() override (nested-write packages such as
drf-writable-nested have one: wrap perform_create in transaction.atomic()
there) or several databases. On PostgreSQL it costs two round trips;
AIODRF = {"ATOMIC_SAVE": False} gives DRF's autocommit behaviour.
An operation that must be atomic across several writes belongs in one
synchronous function with transaction.atomic() inside it, awaited once
through sync_to_async. Keep the connection and the transaction in that
function: neither can move between threads. A synchronous operation that is
already running finishes even if the client disconnects; do not retry writes
automatically.
6. The codemod¶
python -m aiodrf.codemod --diff myproject/api/ # show what would change
python -m aiodrf.codemod --check myproject/api/ # exit 1 if anything would
python -m aiodrf.codemod myproject/api/ # rewrite the files
from rest_framework.<module> import <names> moves each name aiodrf provides
to aiodrf.<module> and leaves the rest on DRF; from rest_framework import
<module> moves when every attribute the file uses exists in aiodrf, and is
kept with a note on stderr otherwise. A directory scan skips environments,
vendored and built code and migrations (the list is in the
adrf guide); a file named on the
command line is always converted, keeping its encoding, line endings and
permissions. A file it cannot parse is named on stderr (not parsed) and
left as it is; the others are still converted, and the run exits with
status 2.
It changes imports, not behaviour: blocking calls inside async def code and
transaction boundaries are the project's to review. A second run produces no
diff.
7. Caching, schema, serializer backends¶
django.views.decorators.cache.cache_pageon an async handler looks the response up synchronously on the event loop. Useaiodrf.cache.cache_pagethere; on a synchronous handler Django's is fine. See the ecosystem guide.- The schema is drf-spectacular's as before. Compare the generated schema before and after the move.
SERIALIZER_BACKEND = "msgspec"or"pydantic"is an optimization for list endpoints, enabled per serializer first. See the serializer guide.
8. Rollout¶
Move one endpoint, run its recorded requests against it, and compare: status,
headers, bodies and error codes, the schema, the query count. count_hops()
from aiodrf.test shows where a request leaves the event loop:
with count_hops() as hops:
response = await AsyncAPIClient().get("/books/")
assert hops.calls == ["ListModelMixin._list"]
Keep the DRF view importable until the endpoint has run in production; switching back is one import.
A sample DRF application converted with the codemod behaves as before with mixed base classes, serializer context, django-filter, denied requests, exceptions in custom actions and the generated schema. Check your own custom hooks after converting.
Generic model serializers need no runtime patch to Django or DRF:
from aiodrf import serializers
class BookSerializer(serializers.ModelSerializer[Book]):
class Meta:
model = Book
fields = ["id", "title"]
The package ships type information (py.typed), including for the optional
contrib modules, so type checkers verify code like this.