skip to content

In Django REST Framework, how do URLPathVersioning, NamespaceVersioning and AcceptHeaderVersioning set request.version, and how do you serve a different serializer for v2?

level: middleimportance: should knowfreq 40%

answer

  1. off by default, version is None
  2. URL kwarg, namespace or media parameter
  3. ALLOWED_VERSIONS and DEFAULT_VERSION
  4. 404 versus 406 for bad versions
  5. branch in get_serializer_class()

basics

~10 s

With a versioning class set, DRF fills request.version before the handler: URLPathVersioning from the version URL kwarg, NamespaceVersioning from the URL namespace, AcceptHeaderVersioning from Accept's version parameter. Branch on it in get_serializer_class().

solid answer

~40 s

Versioning is off by default: `DEFAULT_VERSIONING_CLASS` is `None` and `request.version` is `None`. Once a scheme is set globally or as `versioning_class` on a view, `APIView.initial()` calls its `determine_version()` after content negotiation and before authentication. `URLPathVersioning` reads the `version` keyword argument captured by the URL pattern; `NamespaceVersioning` reads the namespace of the matched `include()`, such as `v2`; `AcceptHeaderVersioning` reads the `version` parameter of the Accept media type, as in `application/json; version=2.0`. `ALLOWED_VERSIONS` restricts values (`DEFAULT_VERSION` always counts as allowed); a disallowed version is a 404 for the URL schemes and a 406 for the header scheme. Serve v2 by returning a different serializer from `get_serializer_class()` when `self.request.version == 'v2'`.

code

python · 29 lines
python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
    "ALLOWED_VERSIONS": ["v1", "v2"],
    "DEFAULT_VERSION": "v1",
}

# urls.py
from django.urls import include, path

urlpatterns = [
    path("v1/", include("jobs.urls", namespace="v1")),
    path("v2/", include("jobs.urls", namespace="v2")),
]

# jobs/views.py
from rest_framework import viewsets

from jobs.models import Job
from jobs.serializers import JobSerializerV1, JobSerializerV2


class JobViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Job.objects.all()

    def get_serializer_class(self):
        if self.request.version == "v2":
            return JobSerializerV2
        return JobSerializerV1

go deeper

for a junior

Know that request.version is None until a versioning class is configured, and that views branch on it.

for a middle

Contrast how the three schemes find the version, the ALLOWED_VERSIONS and DEFAULT_VERSION settings, and the 404 versus 406 outcomes.

for a senior

Keep versions from leaking across code paths: allow-lists, per-version tests, version-aware reverse(), and a plan for retiring v1 handlers.

for a principal

Own the versioning policy across teams: which scheme the organisation standardises on and how long old versions are served, with DRF as the mechanism.

## Versioning in DRF at a glance Django REST Framework's **versioning classes** read an API version from the request and expose it as `request.version`, together with the scheme instance on `request.versioning_scheme`. The relevant settings: | Setting | Default | Meaning | |---|---|---| | `DEFAULT_VERSIONING_CLASS` | `None` | no versioning; `request.version` is always `None` | | `DEFAULT_VERSION` | `None` | value used when the request carries no version | | `ALLOWED_VERSIONS` | `None` | if set, any other value is rejected | | `VERSION_PARAM` | `'version'` | name of the URL kwarg, media-type parameter or query parameter | A view can override the scheme with `versioning_class`, though one scheme for the whole API is the usual choice. `DEFAULT_VERSION` is always treated as allowed even when it is missing from `ALLOWED_VERSIONS` (unless it is `None`). ## Where the version is determined `APIView.initial()` runs, in order: 1. content negotiation, which sets `request.accepted_renderer`; 2. `determine_version()`, which sets `request.version` and `request.versioning_scheme`; 3. authentication, permission checks and throttling. So permission classes, throttles and the handler can all read `request.version`. ## The three schemes this leaf covers - **`URLPathVersioning`**: the URL pattern captures a `version` kwarg, e.g. `re_path(r'^(?P<version>(v1|v2))/jobs/$', ...)`. The scheme reads it from the resolver kwargs. The kwarg is also passed on to the handler, so method signatures must accept `**kwargs` or a `version` argument. - **`NamespaceVersioning`**: the same URLs to the client, but configured with `path('v1/', include('jobs.urls', namespace='v1'))` and `path('v2/', include(..., namespace='v2'))`. The version is the namespace of the matched route; nested namespaces are split on `:` and the first allowed one wins. No `version` kwarg reaches the view. - **`AcceptHeaderVersioning`**: the URL stays unversioned and the client sends `Accept: application/json; version=2.0`. The scheme reads the parameter from the **negotiated** media type, which is why negotiation runs first. DRF also ships `HostNameVersioning` and `QueryParameterVersioning` (`?version=`). ## What happens with a bad version | Scheme | Error for a version outside `ALLOWED_VERSIONS` | |---|---| | `URLPathVersioning` | `NotFound` (404), 'Invalid version in URL path.' | | `NamespaceVersioning` | `NotFound` (404) | | `AcceptHeaderVersioning` | `NotAcceptable` (406) | Without `ALLOWED_VERSIONS`, any captured value passes through, so a regex like `(?P<version>[^/]+)` would let `/v9/jobs/` reach your views with `request.version == 'v9'`. ## Serving v1 and v2 from one codebase - **Serializer choice**: override `get_serializer_class()` and return `JobSerializerV2` when `self.request.version == 'v2'`. This is the pattern DRF's docs show. - **Behaviour switches**: branch on `request.version` in `get_queryset()` or the handler only for small differences; large divergence is clearer as separate view classes routed per version. - **Links**: build URLs with `rest_framework.reverse.reverse(..., request=request)` so the scheme keeps the caller's version in them. - **Tests**: exercise each version explicitly, since a view that forgets to branch silently serves v1 data under a v2 URL. Whether to version by path, header or media type, and when to cut a new version at all, is an API-design decision rather than a DRF one.

  • Why does AcceptHeaderVersioning answer 406 for a bad version while URLPathVersioning answers 404?
    The header scheme reads the version from the negotiated media type, so an unsupported version means the requested representation cannot be produced: `NotAcceptable`, 406. The path scheme treats the version as part of the resource address, so an unknown one is `NotFound`, 404.
  • With URLPathVersioning, a handler written as def get(self, request, pk) starts failing. Why?
    The URL pattern captures `version` as a keyword argument, and DRF passes all resolver kwargs on to the handler. `get()` then receives an unexpected `version` argument and raises `TypeError`. Accept `*args, **kwargs` or a `version` parameter, or use `NamespaceVersioning`, which does not add a kwarg.

saying these in an interview costs you the question

  • DRF sets request.version to 'v1' when versioning is not configured
  • An invalid version always produces 404 whatever the scheme
  • AcceptHeaderVersioning reads a custom X-API-Version header
  • ALLOWED_VERSIONS must also list DEFAULT_VERSION or requests without a version fail
  • NamespaceVersioning passes a version keyword argument to the view