-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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 schemastype 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{}}
})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
)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.
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.
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.
rextension-validation — request/response validation and content negotiation for Rex · MIT · © 2026 Kryovyx