skip to content

What are vendor media types such as `application/vnd.github+json` in an HTTP Accept header, and how are they used to select an API version?

level: seniorimportance: should knowfreq 34%

answer

  1. vnd. = vendor tree; standards tree has no prefix
  2. +json = structured syntax suffix, parse as JSON
  3. version in subtype vs as a parameter
  4. echo the exact type in Content-Type
  5. Vary: Accept + normalise at the CDN

basics

~20 s

They are media types in the vendor registration tree (vnd.) naming an organisation's own format, usually with a structured suffix like +json. A client asks for one in Accept, e.g. application/vnd.example.v2+json, and the server returns that variant and echoes it in Content-Type.

solid answer

~50 s

IANA splits media types into registration **trees**. Anything under `vnd.` is the vendor tree — a format owned by a specific organisation rather than a generic one. The `+json` **structured syntax suffix** says "the underlying serialisation is JSON", so generic tooling can still parse it while the prefix carries the domain meaning. Because the media type is a negotiable axis, teams encode a version in it and negotiate on it: ``` Accept: application/vnd.example.v2+json ``` or with a parameter: `application/vnd.example+json;version=2`. The server returns that variant and sets `Content-Type` to exactly the type it produced, plus `Vary: Accept`. Tradeoffs on the wire: the version is invisible in the URL (harder to eyeball in logs, harder to curl), every representation shares a cache key axis so `Vary: Accept` is mandatory, and clients that default to `Accept: */*` must be given a sane default. In exchange, one URL identifies the resource for its whole lifetime and the format is versioned independently of the resource identity.

code

bash · 2 lines
bash
curl -i https://api.example.com/users/42 \
  -H 'Accept: application/vnd.example+json;version=2'

go deeper

for a junior

Recognise the shape: vnd. means a vendor-owned format and +json means the bytes are JSON. Know that the server echoes the chosen type in Content-Type.

for a middle

Explain the two version encodings (subtype vs parameter), and why the response must name the exact type produced.

for a senior

Discuss operational reality: Vary: Accept and CDN cache fragmentation, Accept normalisation at the edge, the */* default path, and middleware that must match the +json suffix.

for a principal

Argue when the format is the thing changing versus the resource model, and set the organisation-wide default/sunset policy for clients that never set Accept.

## Media type registration trees IANA organises media type names into trees, signalled by a prefix on the subtype: - **Standards tree** — no prefix: `application/json`, `text/html`. Registered through a formal process. - **Vendor tree** — `vnd.`: `application/vnd.ms-excel`, `application/vnd.api+json`. Owned by a product or organisation. - **Personal/vanity tree** — `prs.`. - **Unregistered tree** — `x.` (the old `x-` convention is discouraged). So `application/vnd.github+json` reads as: an application-level format, owned by a vendor, called `github`, serialised as JSON. ## Structured syntax suffixes The `+json` tail is a **structured syntax suffix** (RFC 6839). It tells any recipient that whatever the domain semantics, the bytes are JSON and can be parsed by a JSON parser. Other suffixes exist: `+xml`, `+cbor`, `+zip`. Common examples you will meet on the wire: - `application/problem+json` — RFC 9457 error documents - `application/vnd.api+json` — JSON:API - `application/ld+json` — JSON-LD A client that does not recognise the prefix can still fall back to "it's JSON". This is why generic middleware should test for the `+json` suffix, not for string equality with `application/json` — a JSON body served as `application/vnd.example.v2+json` will otherwise be rejected by naive parsers, logging filters, and body-reading frameworks. ## Versioning through the Accept header Two encodings are used: 1. **In the subtype:** `Accept: application/vnd.example.v2+json` 2. **As a parameter:** `Accept: application/vnd.example+json;version=2` Form 2 is cleaner: the media type identity stays stable and the version is an ordinary media-type parameter, so a range like `application/vnd.example+json` still matches every version (with matching against parameters getting stricter as clients specify them). Form 1 creates a new type name per version, which means `*/*`-style matching and generic tooling see unrelated types. The exchange looks like this: ``` GET /users/42 HTTP/1.1 Accept: application/vnd.example+json;version=2 HTTP/1.1 200 OK Content-Type: application/vnd.example+json;version=2 Vary: Accept ``` The response **must** echo the concrete type actually produced. A server that serves v1 while claiming v2 — or that returns bare `application/json` when the client asked for a specific vendor type — makes the negotiation unverifiable. ## What this costs on the wire - **`Vary: Accept` is mandatory.** The body depends on a request header, so every cache and CDN must key on it. Because `Accept` values from real clients are wildly variable strings, this can fragment a CDN cache badly; production setups often normalise `Accept` at the edge into a small set of buckets before it reaches the cache key. - **Debuggability.** You cannot paste the URL in a browser and see v2 — the browser sends its own Accept. Every curl needs `-H`. - **Default behaviour.** Most HTTP clients default to `Accept: */*`. That range matches your vendor type, so the server must have a well-defined default version for those clients — usually the oldest supported, so nobody is silently upgraded, or a pinned version with a documented sunset. - **Unsatisfiable requests.** If a client asks for `version=9`, the server has to decide between refusing and serving something else — a decision with its own status-code implications. ## When it is a good fit It fits when the **representation format** changes but the **resource identity** does not: the same user, described differently. It fits badly when the change is really a different resource model or different routing, and it fits badly when your consumers are browsers or ad-hoc scripts that will never set the header. ## Anti-patterns - Inventing `application/x-mycompany-json` — the `x-` convention is deprecated; use `vnd.`. - Omitting the `+json` suffix, which strands every generic JSON parser and body decoder in the path. - Putting the version in `Accept` *and* the URL and letting them disagree. - Failing to echo the negotiated type in `Content-Type`, so clients cannot tell which variant they got.

  • Why does the `+json` suffix matter to middleware in the request path?
    Because generic components — body parsers, logging redactors, gateways, browser devtools — decide how to treat a payload from its media type. If they compare for equality with `application/json` they will treat `application/vnd.example+json` as opaque bytes. Testing for the `+json` suffix keeps a vendor type parseable everywhere.
  • What happens to CDN caching when you negotiate versions through Accept?
    The response varies by a header whose real-world values are long and inconsistent, so `Vary: Accept` explodes cache cardinality and lowers hit rate. The usual fix is to normalise Accept at the edge into a small set of canonical values before it reaches the cache key, so only the versions you actually serve create distinct entries.
  • A client sends `Accept: */*` to a vendor-media-type API. Which version does it get?
    `*/*` matches every vendor type, so the server falls back to its configured default. That default should be explicit and stable — commonly the oldest supported version, so existing scripts are never silently upgraded — and it must be documented, because most client libraries send `*/*` without the developer realising.

saying these in an interview costs you the question

  • Using an `x-` prefixed media type instead of the `vnd.` tree.
  • Dropping the `+json` suffix and expecting generic parsers to cope.
  • Returning plain `application/json` after the client requested a specific vendor type.
  • Forgetting `Vary: Accept` and then blaming the CDN for serving the wrong version.
  • Assuming every client sets Accept, when most libraries default to `*/*`.

context