Skip to content

Codecs and Negotiation

wiki edited this page Sep 4, 2026 · 1 revision

Codecs and negotiation

type Codec interface {
	ContentType() string
	Marshal(v interface{}) ([]byte, error)
	Unmarshal(data []byte, v interface{}) error
}

JSONCodec is registered by default and is the only one out of the box.

Content-Type — the request side

A request body whose Content-Type matches no registered codec is 415:

{"type":"urn:rex:problem:unsupported-media-type","title":"Unsupported Media Type",
 "status":415,"detail":"the request body's content type is not supported"}

Only for routes that declare a RequestBody(). A route that accepts no body does not care what a client labelled one.

Accept — the response side

The Accept header is negotiated against the registered codecs. The first registered codec is the default when a client sends no Accept header — which is why JSONCodec is first unless you change it.

No acceptable representation is 406.

The negotiated codec is used to re-encode the response, so a handler writes a Go value and the client gets whatever it asked for:

codec := validation.GetAcceptCodec(ctx.Request()) // nil if the route declares no schemas

Adding a codec

type XMLCodec struct{}

func (XMLCodec) ContentType() string                    { return "application/xml" }
func (XMLCodec) Marshal(v interface{}) ([]byte, error)  { return xml.Marshal(v) }
func (XMLCodec) Unmarshal(d []byte, v interface{}) error { return xml.Unmarshal(d, v) }

app := rex.New(validation.WithValidation(validation.NewConfig(
	validation.WithCodec(XMLCodec{}),
)))

WithCodec appends, so JSON stays the default. To make another codec the default, build the list yourself with it first:

validation.NewConfig(func(c *validation.Config) {
	c.Codecs = []validation.Codec{XMLCodec{}, validation.JSONCodec{}}
})

Strict decoding

type StrictCodec interface {
	Codec
	UnmarshalStrict(data []byte, v interface{}) error
}

Required by every registered codec when unknown-field rejection is on — which it is by default. A codec that cannot do it fails the boot rather than decoding leniently while the configuration says otherwise.

func (XMLCodec) UnmarshalStrict(d []byte, v interface{}) error {
	dec := xml.NewDecoder(bytes.NewReader(d))
	dec.Strict = true
	return dec.Decode(v)
}

It is a separate interface rather than a parameter on Unmarshal so a third-party codec keeps compiling. What it does not get is silence.

If your codec genuinely cannot enforce it, say so explicitly:

validation.NewConfig(
	validation.WithCodec(LenientCodec{}),
	validation.WithAllowUnknownFields(), // ← the deliberate statement
)

The JSON codec

Unmarshal ignores undeclared members; UnmarshalStrict rejects them. Both are provided, so JSON works under either configuration.

Marshal is still encoding/json v1 on purpose: response bodies are marshalled from types the application owns, where v1 and v2 agree, and v1 keeps the encoding of any type that already implements MarshalJSON.

Content negotiation and errors

Note the asymmetry, and it is deliberate: problem documents ignore Accept. rextension.Problem.Write always sets application/problem+json.

Negotiation applies to success responses. A client that asked for application/xml and then made a mistake is better served by a machine-readable problem document it did not ask for than by a 406 carrying no information about what actually went wrong. RFC 9457 §3 anticipates this.

Charsets and parameters

Media type parameters — application/json; charset=utf-8 — are matched on the base type. A codec registers application/json and matches all of its parameterised forms.

Clone this wiki locally