Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions bindform.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package runtime

import (
"encoding/json"
"errors"
"fmt"
"mime/multipart"
Expand Down Expand Up @@ -69,7 +68,7 @@ func BindForm(ptr interface{}, form map[string][]string, files map[string][]*mul
value := values[0]
if encoding.ContentType != "" {
if strings.HasPrefix(encoding.ContentType, jsonContentType) {
if err := json.Unmarshal([]byte(value), ptr); err != nil {
if err := JSONUnmarshal([]byte(value), ptr); err != nil {
return err
}
}
Expand Down Expand Up @@ -122,7 +121,7 @@ func MarshalForm(ptr interface{}, encodings map[string]RequestBodyEncoding) (url
tag = strings.Split(tag, ",")[0] // extract the name of the tag
if encoding, ok := encodings[tag]; ok && encoding.ContentType != "" {
if strings.HasPrefix(encoding.ContentType, jsonContentType) {
if data, err := json.Marshal(field); err != nil { //nolint:staticcheck
if data, err := JSONMarshal(field); err != nil { //nolint:staticcheck
return nil, err
} else {
result[tag] = append(result[tag], string(data))
Expand Down
18 changes: 12 additions & 6 deletions bindjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
Expand All @@ -31,17 +32,22 @@ type validatedBody interface {
// case-insensitively, and the body is not decoded a second time.
//
// Without a validated value, or when the value cannot be bound into v, the
// body is decoded with encoding/json exactly as before, so error behaviour
// (including io.EOF for an empty body) is unchanged.
// body is decoded with JSONUnmarshalRead: the current JSONCodec (see
// SetJSONCodec), as a stream when it implements StreamingJSONCodec. With the
// default codec that is encoding/json's Decoder, so its error behaviour is
// unchanged. A body that is empty, or only white space, is io.EOF whichever
// codec is in use: generated handlers take that as an optional body left out.
func BindJSONBody(r *http.Request, v any) error {
var body io.Reader
if r != nil && r.Body != nil {
if b, ok := r.Body.(validatedBody); ok {
if err := BindJSONValue(b.ValidatedValue(), v); err == nil {
return nil
}
}
body = r.Body
}
return json.NewDecoder(r.Body).Decode(v)
return JSONUnmarshalRead(body, v)
}

// BindJSONValue binds a decoded JSON value into v, which must be a non-nil
Expand Down Expand Up @@ -80,7 +86,7 @@ var (
)

// errUnsupportedJSONDestination is returned for destination types the binder
// does not handle; BindJSONBody then falls back to encoding/json.
// does not handle; BindJSONBody then decodes the body with the JSONCodec.
var errUnsupportedJSONDestination = errors.New("unsupported destination type")

func bindJSONValue(value any, dst reflect.Value) error {
Expand All @@ -102,7 +108,7 @@ func bindJSONValue(value any, dst reflect.Value) error {
}

if u := jsonUnmarshalerOf(dst); u != nil {
data, err := json.Marshal(value)
data, err := JSONMarshal(value)
if err != nil {
return err
}
Expand Down Expand Up @@ -295,7 +301,7 @@ func bindQuotedJSONValue(value any, dst reflect.Value) error {
if !ok || !dst.CanAddr() {
return jsonTypeError(value, dst.Type())
}
return json.Unmarshal([]byte(s), dst.Addr().Interface())
return JSONUnmarshal([]byte(s), dst.Addr().Interface())
}

func jsonUnmarshalerOf(v reflect.Value) json.Unmarshaler {
Expand Down
3 changes: 1 addition & 2 deletions bindparam.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ package runtime

import (
"encoding"
"encoding/json"
"errors"
"fmt"
"net/url"
Expand Down Expand Up @@ -380,7 +379,7 @@ func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode
}
}
jsonParam := "{" + strings.Join(fields, ",") + "}"
err := json.Unmarshal([]byte(jsonParam), dest)
err := JSONUnmarshal([]byte(jsonParam), dest)
if err != nil {
return fmt.Errorf("error binding parameter %s fields: %w", paramName, err)
}
Expand Down
2 changes: 1 addition & 1 deletion deepobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func MarshalDeepObject(i interface{}, paramName string) (string, error) {
return "", nil
}
}
buf, err := json.Marshal(i)
buf, err := JSONMarshal(i)
if err != nil {
return "", fmt.Errorf("failed to marshal input to JSON: %w", err)
}
Expand Down
125 changes: 125 additions & 0 deletions internal/jsoncodec/jsoncodec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Package jsoncodec holds the process-wide JSON codec that the runtime package
// and runtime/types share.
//
// It is internal because runtime imports types, so types cannot import
// runtime. The runtime package re-exports its symbols, as aliases of the types
// and one-line wrappers of the functions, so that nothing is defined twice.
package jsoncodec

import (
"bytes"
"encoding/json"
"io"
)

// Codec encodes and decodes JSON. runtime.JSONCodec is an alias of it.
type Codec interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}

// Streaming is optionally implemented by a Codec that encodes to and decodes
// from a stream. runtime.StreamingJSONCodec is an alias of it.
type Streaming interface {
MarshalWrite(w io.Writer, v any) error
UnmarshalRead(r io.Reader, v any) error
}

// Std is encoding/json's json.Marshal and json.Unmarshal.
// runtime.StdJSONCodec is an alias of it.
//
// It deliberately doesn't implement Streaming: a codec that embeds it and
// overrides Marshal or Unmarshal then encodes and decodes streams through
// its own methods, via MarshalWrite and UnmarshalRead's fall-through, rather
// than through encoding/json's Encoder and Decoder promoted from Std.
type Std struct{}

func (Std) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
func (Std) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }

// std is the default codec: Std, plus encoding/json's Encoder and Decoder for
// streams, so that the default behaviour is encoding/json's throughout.
type std struct{ Std }

func (std) MarshalWrite(w io.Writer, v any) error { return json.NewEncoder(w).Encode(v) }
func (std) UnmarshalRead(r io.Reader, v any) error { return json.NewDecoder(r).Decode(v) }

var _ Streaming = std{}

var codec Codec = std{}

// Set replaces the codec. nil restores the default, and so does Std itself:
// it is the default's byte half, and passing it is the natural way to say
// "encoding/json", not a request for the buffered stream path.
func Set(c Codec) {
if c == nil || c == (Std{}) {
c = std{}
}
codec = c
}

// Marshal encodes v with the codec.
func Marshal(v any) ([]byte, error) { return codec.Marshal(v) }

// Unmarshal decodes data into v with the codec.
func Unmarshal(data []byte, v any) error { return codec.Unmarshal(data, v) }

// MarshalWrite encodes v to w: with the codec's MarshalWrite when it
// implements Streaming, otherwise with Marshal followed by a single Write.
func MarshalWrite(w io.Writer, v any) error {
if s, ok := codec.(Streaming); ok {
return s.MarshalWrite(w, v)
}
data, err := codec.Marshal(v)
if err != nil {
return err
}
_, err = w.Write(data)
return err
}

// UnmarshalRead decodes v from r: with the codec's UnmarshalRead when it
// implements Streaming, otherwise by reading r to its end and calling
// Unmarshal. When r is nil or holds no value, nothing or only JSON white
// space, it returns io.EOF without involving the codec, whose own report of
// an empty input varies.
func UnmarshalRead(r io.Reader, v any) error {
r, err := nonEmpty(r)
if err != nil {
return err
}
if s, ok := codec.(Streaming); ok {
return s.UnmarshalRead(r, v)
}
data, err := io.ReadAll(r)
if err != nil {
return err
}
return codec.Unmarshal(data, v)
}

// nonEmpty returns io.EOF when r is nil or has only JSON white space before
// its end, and otherwise r with the bytes read to find that out replayed in
// front of it, so that the codec sees the input unchanged, a syntax error's
// offset included.
func nonEmpty(r io.Reader) (io.Reader, error) {
if r == nil {
return nil, io.EOF
}
var read []byte
for {
var b [1]byte
n, err := r.Read(b[:])
if n == 1 {
read = append(read, b[0])
switch b[0] {
case ' ', '\t', '\r', '\n':
continue
}
return io.MultiReader(bytes.NewReader(read), r), nil
}
if err != nil {
return nil, err
}
}
}
87 changes: 87 additions & 0 deletions jsoncodec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package runtime

import (
"io"

"github.com/oapi-codegen/runtime/internal/jsoncodec"
)

// JSONCodec encodes and decodes JSON for this package and for the code
// oapi-codegen generates. It has two methods:
//
// Marshal(v any) ([]byte, error)
// Unmarshal(data []byte, v any) error
//
// A codec must produce and accept standard JSON: it selects an implementation
// and its options, such as encoding/json/v2 or an encoder that doesn't escape
// HTML, not a different format. A codec that also implements
// StreamingJSONCodec encodes and decodes request and response bodies as
// streams.
//
// A codec must be re-entrant. The MarshalJSON and UnmarshalJSON methods of
// the types in runtime/types, and of the union and additionalProperties
// types oapi-codegen generates, call back into the codec from inside the
// codec's own Marshal or Unmarshal of a value that contains them. A codec
// that holds a lock across Marshal deadlocks on the first such value.
type JSONCodec = jsoncodec.Codec

// StreamingJSONCodec is optionally implemented by a JSONCodec that encodes to
// and decodes from a stream, which request and response bodies use:
//
// MarshalWrite(w io.Writer, v any) error
// UnmarshalRead(r io.Reader, v any) error
//
// Without it, JSONMarshalWrite and JSONUnmarshalRead buffer the whole body
// through Marshal and Unmarshal. Whether MarshalWrite ends the value with a
// newline, and whether UnmarshalRead rejects data after the value, is up to
// the implementation: encoding/json's Encoder and Decoder do and don't,
// respectively. Neither this package nor generated code relies on either.
// UnmarshalRead is never handed an empty input: see JSONUnmarshalRead.
type StreamingJSONCodec = jsoncodec.Streaming

// StdJSONCodec is encoding/json's json.Marshal and json.Unmarshal. Embed it in
// a codec to replace one method.
//
// It doesn't implement StreamingJSONCodec, so a codec that embeds it handles
// bodies through its own Marshal and Unmarshal, buffered. That differs from
// the default in two ways: a response body has no trailing newline, and data
// after a request body's value is rejected where encoding/json's Decoder
// ignores it. SetJSONCodec treats StdJSONCodec itself as the default, so
// passing it doesn't select the buffered path.
type StdJSONCodec = jsoncodec.Std

// SetJSONCodec replaces the codec used by this package and by the code
// oapi-codegen generates, process-wide. It is for a program's main package to
// call during initialisation, not for a library: it is not synchronised, and
// it changes the JSON of every generated package linked into the program,
// including those of other libraries. nil restores the default, encoding/json
// throughout, and so does StdJSONCodec.
//
// The codec covers what this package and generated code encode and decode
// themselves. It doesn't reach JSON that a web framework handles on their
// behalf: the strict servers for echo, gin, fiber and iris decode request
// bodies with the framework's binder, fiber's and iris's write responses with
// the framework's JSON method, and a request body a validation middleware
// decoded and kept (see BindJSONBody) is bound without decoding it again.
func SetJSONCodec(c JSONCodec) { jsoncodec.Set(c) }

// JSONMarshal encodes v with the current JSONCodec.
func JSONMarshal(v any) ([]byte, error) { return jsoncodec.Marshal(v) }

// JSONUnmarshal decodes data into v with the current JSONCodec.
func JSONUnmarshal(data []byte, v any) error { return jsoncodec.Unmarshal(data, v) }

// JSONMarshalWrite encodes v to w with the current JSONCodec: with its
// MarshalWrite when it implements StreamingJSONCodec, otherwise with Marshal
// followed by a single Write.
func JSONMarshalWrite(w io.Writer, v any) error { return jsoncodec.MarshalWrite(w, v) }

// JSONUnmarshalRead decodes v from r with the current JSONCodec: with its
// UnmarshalRead when it implements StreamingJSONCodec, otherwise by reading r
// to its end and calling Unmarshal.
//
// When r is nil or holds no value, nothing or only JSON white space, it
// returns io.EOF whichever codec is in use, as encoding/json's Decoder does,
// and the codec isn't involved: generated handlers take io.EOF as an optional
// request body left out. Otherwise the codec sees r's input unchanged.
func JSONUnmarshalRead(r io.Reader, v any) error { return jsoncodec.UnmarshalRead(r, v) }
Loading
Loading