Skip to content

Media Type API Versioning — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

Can I use media type versioning with XML?

Yes: application/vnd.myapp.v2+xml. The +xml suffix indicates XML format.

How do API clients discover available media types?

Use OPTIONS requests or documentation. Consider returning supported media types in the response header.

Are vnd media types standardized?

They are de facto standard. IANA reserves the vnd. tree for vendor use. They are widely adopted in REST APIs.

Can I mix media type and URI versioning?

Avoid mixing. Choose one strategy and stick with it. Mixing confuses clients and complicates routing.

How do I handle media types in OpenAPI specs?

Use the 'produces' field (Swagger 2.0) or 'content' field (OpenAPI 3.x) to list supported media types per endpoint.

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