Repository navigation
feat: add process-wide JSON codec - #183
Conversation
Add a JSONCodec that generated code and this package share, so the JSON implementation can be replaced in one place. - Add JSONCodec, StdJSONCodec, SetJSONCodec, JSONMarshal and JSONUnmarshal. - Route the json.Marshal and json.Unmarshal calls in bindform.go, bindjson.go, bindparam.go and deepobject.go through the codec. - Route the MarshalJSON and UnmarshalJSON methods in types/ through the codec. - Keep the codec in internal/jsoncodec, because runtime imports types and types cannot import runtime. Co-Authored-By: Claude Fable 5.2 <[email protected]>
|
@CRaLFa thanks, this is a clean implementation and the 1. Re-export the internal package instead of duplicating itThe interface is declared twice ( type JSONCodec = jsoncodec.Codec
type StdJSONCodec = jsoncodec.Std
func SetJSONCodec(c JSONCodec) { jsoncodec.Set(c) }
func JSONMarshal(v any) ([]byte, error) { return jsoncodec.Marshal(v) }
func JSONUnmarshal(data []byte, v any) error { return jsoncodec.Unmarshal(data, v) }Godoc won't expand the method set under an alias to an internal type, so the doc comments on the aliases will spell the methods out. We're fine with that trade. 2. Add an optional streaming interfaceBodies are where large payloads live, and a byte-slice-only codec forecloses streaming for them forever. Adding methods to // StreamingJSONCodec is optionally implemented by a JSONCodec. Body sites use it
// when present and otherwise buffer through Marshal and Unmarshal.
type StreamingJSONCodec interface {
MarshalWrite(w io.Writer, v any) error
UnmarshalRead(r io.Reader, v any) error
}
func JSONMarshalWrite(w io.Writer, v any) error // uses MarshalWrite if implemented, else Marshal + w.Write
func JSONUnmarshalRead(r io.Reader, v any) error // uses UnmarshalRead if implemented, else io.ReadAll + UnmarshalSingle One subtlety: the exported, embeddable 3.
|
…ckage Follow-up to the process-wide JSON codec, per the review on oapi-codegen#183. Re-export rather than duplicate: the internal/jsoncodec package now also holds the default implementation, and the top-level file is type aliases and one-line wrappers, so neither the interface nor the default is defined twice. Add StreamingJSONCodec, optionally implemented by a JSONCodec, with MarshalWrite(io.Writer, any) and UnmarshalRead(io.Reader, any), and the helpers JSONMarshalWrite and JSONUnmarshalRead that use those methods when present and otherwise buffer through Marshal and Unmarshal. One SetJSONCodec registration covers both, so a codec that implements only the byte pair behaves coherently everywhere, and one that also streams is picked up automatically. The method names are encoding/json/v2's. StdJSONCodec implements only the byte pair. A codec that embeds it and overrides Marshal must not have encoding/json's Encoder promoted past that override for bodies. The default codec, which SetJSONCodec(nil) restores, is unexported and adds encoding/json's Encoder and Decoder, so behaviour without SetJSONCodec is unchanged. BindJSONBody decodes through JSONUnmarshalRead. Generated handlers take io.EOF as an optional body left out, and neither json.Unmarshal nor v2 reports empty input that way, so BindJSONBody checks for an empty or white-space-only body itself and returns io.EOF before the codec is involved. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Review follow-ups to the streaming codec.
JSONUnmarshalRead, not only BindJSONBody, now returns io.EOF when its
input is nil, empty or only JSON white space, whichever codec is in use
and without involving it. The helper is exported for generated code, and
a handler that checks errors.Is(err, io.EOF) must not depend on how the
registered codec reports an empty input: encoding/json's Decoder says
io.EOF, json.Unmarshal says "unexpected end of JSON input", and v2 says
something else again. The bytes read to find the first non-white-space
byte are replayed in front of the reader, so the codec sees the input
unchanged and a syntax error's offset is what it was before.
SetJSONCodec(StdJSONCodec{}) restores the default, as nil does. StdJSONCodec
is the default's byte half, and passing it reads as "encoding/json", not
as a request for the buffered stream path with its missing trailing
newline and trailing-data rejection. The StdJSONCodec doc now spells out
those two differences for codecs that embed it.
Document the codec contract: it must be re-entrant, since the MarshalJSON
methods in runtime/types and in generated union and additionalProperties
types call back into it from inside its own Marshal; SetJSONCodec is for a
program's main package, being process-wide and unsynchronised; and the
codec doesn't reach JSON a web framework's binder or JSON method handles,
nor a validated body bound without decoding it again.
Fix two stale comments, and test a read error, the empty-input contract
under nil, byte-only and streaming codecs, and the default path with
leading white space.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Implements step 1 of the plan in oapi-codegen/oapi-codegen#2371 (comment): a single process-wide JSON codec that this package and generated code share.
What this adds
JSONCodec,StdJSONCodec,SetJSONCodec,JSONMarshalandJSONUnmarshal, with the signatures proposed in that comment.json.Marshal/json.Unmarshalcalls inbindform.go,bindjson.go,bindparam.goanddeepobject.gonow go through the codec.MarshalJSON/UnmarshalJSONmethods intypes/(Date,Duration,Email,File) now go through the codec.Behaviour is unchanged unless
SetJSONCodecis called.Differences from the proposal
The codec lives in
internal/jsoncodec.runtimeimportsruntime/types, sotypescannot callruntime.JSONMarshalwithout an import cycle.The public API in
runtimeis as proposed; it delegates to the internal package, andtypescalls the internal package directly.json.NewDecodercall sites are left onencoding/json.The codec has only
MarshalandUnmarshal, so these do not map onto it directly:BindJSONBody(bindjson.go) decodes the request body withjson.NewDecoder(r.Body).Decode(v). Switching it toio.ReadAll+JSONUnmarshalwould change its documented error behaviour (io.EOFfor an empty body) and how trailing data is handled.MarshalDeepObjectandStyleParamWithOptionsdecode intointerface{}withUseNumber()to walk the value generically.BindJSONBodyis the main path for request bodies, so a custom codec does not apply there yet.I left it alone to keep this PR free of behaviour changes, but I'm happy to switch it here or in a follow-up if you'd prefer. Let me know which.
jsonScalarFromString(deepobject.go) still usesjson.Unmarshal.It only tests whether a string is a JSON number, and does not encode or decode user values.
Testing
TestSetJSONCodeccovers replacing the codec, restoring the default withnil, and that bothruntimeinternals andruntime/typesreach the replaced codec.🤖 Generated with Claude Code