Media Type API Versioning — Complete Guide
In this tutorial, you'll learn about Media Type Versioning. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Media type versioning uses custom media types in the Accept header to specify both the format and version of the response, following a type/subtype+format convention.
What You'll Learn
By the end of this lesson, you will implement custom media type versioning with structured suffixes and understand how media types represent versioned resources.
Why It Matters
Media type versioning is the most RESTful approach according to many practitioners. It treats version as part of resource representation, keeping URLs completely clean.
Real-World Use
GitHub uses application/vnd.github.v3+json and application/vnd.github.v3.raw+json for different representations. Stripe uses application/vnd.stripe.v2+json.
Media Type Structure
flowchart LR
A["application/vnd.myapp.v2+json"]
A --> B[type: application]
A --> C[vendor: vnd]
A --> D[vendor-tree: myapp]
A --> E[version: v2]
A --> F[suffix: +json]
Media Type Registry
# media_type_registry.py
from typing import Dict, Optional
class MediaType:
def __init__(self, media_type_str: str):
self.raw = media_type_str
self.vendor = None
self.version = None
self.format = None
self._parse()
def _parse(self):
# application/vnd.vendor.v1+json
# type/vendorTree.vendorName.vVersion+format
parts = self.raw.split("/")
if len(parts) != 2:
return
self.type = parts[0]
rest = parts[1]
if "+" in rest:
rest, self.format = rest.split("+", 1)
# vnd.vendor.v1
segments = rest.split(".")
if len(segments) >= 2 and segments[0] == "vnd":
self.vendor = segments[1] if len(segments) > 1 else None
if len(segments) > 2:
version_str = segments[2]
if version_str.startswith("v"):
try:
self.version = int(version_str[1:])
except ValueError:
self.version = None
def __repr__(self):
return f"MediaType(vendor={self.vendor}, version={self.version}, format={self.format})"
class MediaTypeRegistry:
def __init__(self):
self.handlers: Dict[str, callable] = {}
def register(self, media_type: str, version: int, handler: callable):
self.handlers[f"v{version}:{media_type.split('+')[-1]}"] = handler
def resolve(self, accept: str) -> Optional[callable]:
raw = accept.split(",")[0].strip().split(";")[0].strip()
mt = MediaType(raw)
if mt.vendor and mt.version and mt.format:
key = f"v{mt.version}:{mt.format}"
return self.handlers.get(key)
return None
registry = MediaTypeRegistry()
registry.register("application/vnd.api.v1+json", 1, lambda: {"users": []})
registry.register("application/vnd.api.v2+json", 2, lambda: {"data": [], "meta": {}})
handler = registry.resolve("application/vnd.api.v2+json, application/json")
print(handler() if handler else "No handler found")
Expected output:
{'data': [], 'meta': {}}
Versioned Schema Validation
# media_schema.py
from typing import Any, Dict
class MediaSchemaValidator:
def __init__(self):
self.schemas = {}
def add_schema(self, media_type: str, schema: Dict):
self.schemas[media_type] = schema
def validate(self, media_type: str, data: Dict) -> bool:
schema = self.schemas.get(media_type)
if not schema:
return False
for field, field_type in schema.get("required", []).items():
if field not in data:
return False
if not isinstance(data[field], field_type):
return False
return True
def get_version_fields(self, version: int) -> list:
fields = []
for mt, schema in self.schemas.items():
mt_parsed = MediaType(mt)
if mt_parsed.version == version:
fields.extend(schema.get("required", {}).keys())
return list(set(fields))
class MediaType:
def __init__(self, mt: str):
self.version = None
parts = mt.split("+")[0].split(".")
for p in parts:
if p.startswith("v") and p[1:].isdigit():
self.version = int(p[1:])
validator = MediaSchemaValidator()
validator.add_schema("application/vnd.api.v1+json",
{"required": {"id": int, "name": str}})
validator.add_schema("application/vnd.api.v2+json",
{"required": {"id": int, "name": str, "email": str}})
v1_data = {"id": 1, "name": "Alice"}
v2_data = {"id": 1, "name": "Alice", "email": "alice@example.com"}
print(f"v1 validates v1 data: {validator.validate('application/vnd.api.v1+json', v1_data)}")
print(f"v2 validates v1 data: {validator.validate('application/vnd.api.v2+json', v1_data)}")
print(f"v2 validates v2 data: {validator.validate('application/vnd.api.v2+json', v2_data)}")
Expected output:
v1 validates v1 data: True
v2 validates v1 data: False
v2 validates v2 data: True
Common Mistakes
1. Non-Standard Media Type Format
Using application/json;version=2 instead of application/vnd.api.v2+json. The former is non-standard and may not work with all HTTP libraries.
2. Not Using the Vendor Tree
Custom media types should use the vnd. prefix (vendor tree) to avoid collision with standard media types.
3. Ignoring the +json Suffix
The structured suffix (+json, +xml) allows parsers to fall back to the base format if they do not understand the specific subtype.
4. Overcomplicating with Too Many Media Types
Use one media type per version per format. Avoid creating subtypes for every minor API variation.
5. Not Documenting Media Type Lifecycle
Document when media types are introduced, deprecated, and sunset so clients can plan migrations.
Practice Questions
1. What is a structured suffix in media types?
The part after + (e.g., +json) indicates the underlying data format, allowing fallback Parsing.
2. Why use the vnd. prefix?
It indicates a vendor-specific media type, preventing conflicts with IANA-registered standard media types.
3. How do clients specify the version with media types?
By setting Accept: application/vnd.myapp.v2+json in the request header.
4. What is the difference between media type and header versioning?
Both use headers. Media type versioning is more RESTful (representation-focused), while custom headers are simpler.
Challenge
Design and implement a media type versioning system for a products API with versions 1-3 using application/vnd.products.v{1,2,3}+json with different required fields per version.
FAQ
Mini Project: Media Type Router
# media_router.py
from typing import Any, Dict, Optional
class MediaRouter:
def __init__(self):
self.routes: Dict[str, callable] = {}
def add(self, media_type: str, handler: callable):
self.routes[media_type] = handler
def route(self, accept: str) -> Dict:
for item in accept.split(","):
mt = item.strip().split(";")[0].strip()
if mt in self.routes:
return {"status": 200, "data": self.routes[mt](), "media_type": mt}
return {"status": 406, "error": "Not acceptable"}
router = MediaRouter()
router.add("application/vnd.api.v1+json", lambda: {"version": "v1"})
router.add("application/vnd.api.v2+json", lambda: {"version": "v2", "extra": True})
print(router.route("application/vnd.api.v2+json"))
print(router.route("text/html"))
Expected output:
{'status': 200, 'data': {'version': 'v2', 'extra': True}, 'media_type': 'application/vnd.api.v2+json'}
{'status': 406, 'error': 'Not acceptable'}
What's Next
You understand media type versioning. Next, learn about semantic versioning for APIs, then explore API version strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro