python 108 lines · 4 tabs

Versioning a DRF Resource With Accept-Header Content Negotiation

Shared by codesnips Sep 2026
4 tabs
from rest_framework.versioning import BaseVersioning
from rest_framework.exceptions import NotAcceptable


class AcceptHeaderVersioning(BaseVersioning):
    default_version = "1"
    allowed_versions = {"1", "2"}
    version_param = "version"

    def determine_version(self, request, *args, **kwargs):
        media_type = getattr(request, "accepted_media_type", "") or ""
        version = self._parse_version(media_type) or self.default_version

        if version not in self.allowed_versions:
            raise NotAcceptable(
                "Unsupported version %r. Allowed: %s"
                % (version, ", ".join(sorted(self.allowed_versions)))
            )
        return version

    def _parse_version(self, media_type):
        for part in media_type.split(";"):
            key, _, value = part.strip().partition("=")
            if key == self.version_param:
                return value.strip()
        return None
4 files · python Explain with highlit

This snippet shows how to version a single API resource using HTTP content negotiation rather than URL paths or query parameters. Instead of exposing /v1/orders/ and /v2/orders/, clients ask for a specific representation with a vendor media type such as application/vnd.acme.order+json; version=2 in the Accept header, and the server picks the right serializer. This keeps a resource at one stable URL while its representation evolves — a REST-purist approach that treats versioning as a property of the media type, not the address.

In versioning.py, AcceptHeaderVersioning subclasses DRF's BaseVersioning. It parses the version parameter out of the negotiated media type recorded on request.accepted_media_type, falling back to DEFAULT_VERSION when the client sends no version. Unknown versions raise NotAcceptable, which DRF renders as a 406, so callers get a precise signal instead of silently receiving the wrong shape. The allowed_versions set guards the whole surface in one place.

renderers.py defines the custom media type. VendorJSONRenderer sets media_type to the vendor string so DRF's content negotiation will match it, and overrides get_media_type to echo back the concrete version that was resolved. Registering it makes the vendor type a first-class renderer the negotiator can select against the incoming Accept header.

serializers.py holds two serializers, OrderV1Serializer and OrderV2Serializer, where v2 renames total to amount_due and adds currency — a classic backward-incompatible change that would otherwise break existing clients.

views.py ties it together: OrderViewSet sets versioning_class and lists the vendor renderer, then get_serializer_class maps request.version to the matching serializer. Because the version travels in the header, the same queryset and URL serve both shapes.

The main trade-off is discoverability — header-based versions are harder to test in a browser than a path segment, and caches must vary on Accept. The payoff is clean, long-lived URLs and version logic centralized in the negotiation layer rather than scattered across routes.


Related snips

python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
python
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [

Django URL namespacing and reverse lookups

django python urls
by Priya Sharma 3 tabs
python
import graphene
from graphene_django import DjangoObjectType
from blog.models import Post, Comment


class PostType(DjangoObjectType):

Django GraphQL with Graphene

django python graphql
by Priya Sharma 2 tabs
python
from django.db.models import Count, Avg, Sum, Q, F
from django.views.generic import TemplateView
from products.models import Product, Order, OrderItem


class DashboardView(TemplateView):

Django aggregation with annotate for statistics

django python database
by Priya Sharma 1 tab
python
from rest_framework import permissions


class IsOwner(permissions.BasePermission):
    """Allow only object owner to access."""

Django REST Framework permissions and authorization

django python rest
by Priya Sharma 2 tabs
python
from django.db import models
from django.utils.text import slugify


class Article(models.Model):
    title = models.CharField(max_length=200)

Django model save override for custom logic

django python models
by Priya Sharma 1 tab

Share this code

Here's the card — post it anywhere.

Versioning a DRF Resource With Accept-Header Content Negotiation — share card
Link copied