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
from rest_framework.renderers import JSONRenderer
class VendorJSONRenderer(JSONRenderer):
media_type = "application/vnd.acme.order+json"
format = "vendor"
def get_media_type(self, renderer_context):
request = renderer_context.get("request") if renderer_context else None
version = getattr(request, "version", None)
if version:
return "%s; version=%s" % (self.media_type, version)
return self.media_type
def render(self, data, accepted_media_type=None, renderer_context=None):
renderer_context = renderer_context or {}
# Advertise the resolved version back to the client.
response = renderer_context.get("response")
if response is not None:
response["Content-Type"] = self.get_media_type(renderer_context)
return super().render(data, accepted_media_type, renderer_context)
from rest_framework import serializers
from .models import Order
class OrderV1Serializer(serializers.ModelSerializer):
total = serializers.DecimalField(max_digits=12, decimal_places=2)
class Meta:
model = Order
fields = ["id", "reference", "total", "status", "created_at"]
read_only_fields = ["id", "created_at"]
class OrderV2Serializer(serializers.ModelSerializer):
amount_due = serializers.DecimalField(
source="total", max_digits=12, decimal_places=2
)
currency = serializers.CharField(max_length=3)
class Meta:
model = Order
fields = [
"id",
"reference",
"amount_due",
"currency",
"status",
"created_at",
]
read_only_fields = ["id", "created_at"]
from rest_framework import viewsets
from rest_framework.exceptions import NotAcceptable
from .models import Order
from .renderers import VendorJSONRenderer
from .serializers import OrderV1Serializer, OrderV2Serializer
from .versioning import AcceptHeaderVersioning
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all().order_by("-created_at")
versioning_class = AcceptHeaderVersioning
renderer_classes = [VendorJSONRenderer]
serializer_by_version = {
"1": OrderV1Serializer,
"2": OrderV2Serializer,
}
def get_serializer_class(self):
version = getattr(self.request, "version", None)
try:
return self.serializer_by_version[version]
except KeyError:
raise NotAcceptable("No serializer for version %r" % version)
def get_serializer_context(self):
context = super().get_serializer_context()
context["version"] = getattr(self.request, "version", None)
return context
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
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
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
import graphene
from graphene_django import DjangoObjectType
from blog.models import Post, Comment
class PostType(DjangoObjectType):
Django GraphQL with Graphene
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
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
"""Allow only object owner to access."""
Django REST Framework permissions and authorization
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
Share this code
Here's the card — post it anywhere.