📖 2 min read (~ 400 words).

Vendor MIME types

API versioning by vendor MIME type — application/vnd.acme.v1+json and friends — is a common alternative to /v1/ URL prefixes. The runtime supports it, but each MIME registers as its own entry: the +json structural suffix is not sniffed automatically.

Spec

consumes:
  - application/vnd.acme.v1+json
  - application/vnd.acme.v2+json
produces:
  - application/vnd.acme.v1+json
  - application/vnd.acme.v2+json

Server registration

Both versions decode the same JSON wire format, so they share the codec. They still need separate registrations:

api := untyped.NewAPI(doc).WithJSONDefaults()

api.RegisterConsumer("application/vnd.acme.v1+json", runtime.JSONConsumer())
api.RegisterProducer("application/vnd.acme.v1+json", runtime.JSONProducer())

api.RegisterConsumer("application/vnd.acme.v2+json", runtime.JSONConsumer())
api.RegisterProducer("application/vnd.acme.v2+json", runtime.JSONProducer())

Full source: docs/examples/contenttypes/vendortypes/main.go

JSONConsumer() and JSONProducer() are side-effect free — calling them per registration is fine.

Picking the version inside the handler

The matched Content-Type is on the request context — recover it via runtime.ContentType(r.Header):

handlePost := func(r *http.Request, body any) (any, error) {
	ct, _, _ := runtime.ContentType(r.Header)
	switch ct {
	case "application/vnd.acme.v1+json":
		return handleV1(body)
	case "application/vnd.acme.v2+json":
		return handleV2(body)
	}
	return nil, errors.New(http.StatusUnsupportedMediaType, "unsupported version")
}

Full source: docs/examples/contenttypes/vendortypes/main.go

For the response side, the runtime has already chosen a producer that matches the client’s Accept — your handler returns a value and the matched Producer writes it. If you need the response shape to differ between versions, branch on the negotiated content-type the same way (see Context.ResponseFormat).

Matching rules — what about MIME parameters?

The asymmetric matching rule applies. If your spec lists a parameterised type (application/vnd.acme+json;version=1), an inbound request with no version parameter does not match. Recommend the simpler form — parameter-distinct types are rarely worth the surprise.

For the v0.30 parameter-honouring change and the per-call opt-out, see standalone / content negotiation.

Adding a non-JSON vendor type

The exact same shape — register a separate Consumer/Producer per declared MIME, even when several share the same codec:

import msgpackcodec "example.com/myapp/codecs/msgpack"

api.RegisterConsumer("application/vnd.acme.v1+msgpack", msgpackcodec.Consumer())
api.RegisterProducer("application/vnd.acme.v1+msgpack", msgpackcodec.Producer())

See custom codec for the msgpackcodec package itself.

When not to do this

Vendor MIME types compose poorly with browser clients (Accept: */* is unspecific), with caches that key on URL alone, and with HTTP middleware that inspects the URL. URL-based versioning (/v1/...) sidesteps all three. Pick vendor MIME types when the API is server-to-server and you genuinely need the same URL to serve multiple representations.