In Django REST Framework, how do URLPathVersioning, NamespaceVersioning and AcceptHeaderVersioning set request.version, and how do you serve a different serializer for v2?
answer
- off by default, version is None
- URL kwarg, namespace or media parameter
- ALLOWED_VERSIONS and DEFAULT_VERSION
- 404 versus 406 for bad versions
- branch in get_serializer_class()
basics
~10 sWith 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 sVersioning 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# 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 JobSerializerV1go deeper
Know that request.version is None until a versioning class is configured, and that views branch on it.
Contrast how the three schemes find the version, the ALLOWED_VERSIONS and DEFAULT_VERSION settings, and the 404 versus 406 outcomes.
Keep versions from leaking across code paths: allow-lists, per-version tests, version-aware reverse(), and a plan for retiring v1 handlers.
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