diff --git a/bindform.go b/bindform.go
index d7f3666..cd99b7f 100644
--- a/bindform.go
+++ b/bindform.go
@@ -1,7 +1,6 @@
package runtime
import (
- "encoding/json"
"errors"
"fmt"
"mime/multipart"
@@ -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
}
}
@@ -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))
diff --git a/bindjson.go b/bindjson.go
index 3a14b1a..6187d4d 100644
--- a/bindjson.go
+++ b/bindjson.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io"
"net/http"
"reflect"
"strconv"
@@ -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
@@ -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 {
@@ -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
}
@@ -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 {
diff --git a/bindparam.go b/bindparam.go
index eebd9b2..dec1a57 100644
--- a/bindparam.go
+++ b/bindparam.go
@@ -15,7 +15,6 @@ package runtime
import (
"encoding"
- "encoding/json"
"errors"
"fmt"
"net/url"
@@ -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)
}
diff --git a/deepobject.go b/deepobject.go
index f33a020..150aa48 100644
--- a/deepobject.go
+++ b/deepobject.go
@@ -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)
}
diff --git a/internal/jsoncodec/jsoncodec.go b/internal/jsoncodec/jsoncodec.go
new file mode 100644
index 0000000..9ba34a2
--- /dev/null
+++ b/internal/jsoncodec/jsoncodec.go
@@ -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
+ }
+ }
+}
diff --git a/jsoncodec.go b/jsoncodec.go
new file mode 100644
index 0000000..3595e91
--- /dev/null
+++ b/jsoncodec.go
@@ -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) }
diff --git a/jsoncodec_test.go b/jsoncodec_test.go
new file mode 100644
index 0000000..726e4b3
--- /dev/null
+++ b/jsoncodec_test.go
@@ -0,0 +1,259 @@
+package runtime
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/oapi-codegen/runtime/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// noEscapeCodec overrides Marshal to leave HTML unescaped, and counts the
+// calls that reach either method.
+type noEscapeCodec struct {
+ StdJSONCodec
+ marshals, unmarshals *int
+}
+
+func (c noEscapeCodec) Marshal(v any) ([]byte, error) {
+ *c.marshals++
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ enc.SetEscapeHTML(false)
+ if err := enc.Encode(v); err != nil {
+ return nil, err
+ }
+ return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
+}
+
+func (c noEscapeCodec) Unmarshal(data []byte, v any) error {
+ *c.unmarshals++
+ return c.StdJSONCodec.Unmarshal(data, v)
+}
+
+// streamingCodec implements StreamingJSONCodec, and counts the calls that
+// reach each method.
+type streamingCodec struct {
+ noEscapeCodec
+ writes, reads *int
+}
+
+func (c streamingCodec) MarshalWrite(w io.Writer, v any) error {
+ *c.writes++
+ _, err := io.WriteString(w, "streamed")
+ return err
+}
+
+func (c streamingCodec) UnmarshalRead(r io.Reader, v any) error {
+ *c.reads++
+ *v.(*string) = "streamed"
+ return nil
+}
+
+var _ StreamingJSONCodec = streamingCodec{}
+
+// errReader fails every Read with err.
+type errReader struct{ err error }
+
+func (r errReader) Read([]byte) (int, error) { return 0, r.err }
+
+// TestSetJSONCodec mutates the process-wide codec, so it must not run in
+// parallel with other tests (they run after all sequential tests finish).
+func TestSetJSONCodec(t *testing.T) {
+ t.Cleanup(func() { SetJSONCodec(nil) })
+
+ const in = ""
+
+ // Default: encoding/json, which escapes HTML.
+ escaped, err := json.Marshal(in)
+ require.NoError(t, err)
+ require.NotEqual(t, `""`, string(escaped))
+
+ got, err := JSONMarshal(in)
+ require.NoError(t, err)
+ assert.Equal(t, string(escaped), string(got))
+
+ var marshals, unmarshals int
+ SetJSONCodec(noEscapeCodec{marshals: &marshals, unmarshals: &unmarshals})
+
+ got, err = JSONMarshal(in)
+ require.NoError(t, err)
+ assert.Equal(t, `""`, string(got))
+ assert.Equal(t, 1, marshals)
+
+ var out string
+ require.NoError(t, JSONUnmarshal(got, &out))
+ assert.Equal(t, in, out)
+ assert.Equal(t, 1, unmarshals)
+
+ // The runtime's own encoding goes through the codec too.
+ _, err = MarshalDeepObject(map[string]string{"k": "v"}, "p")
+ require.NoError(t, err)
+ assert.Equal(t, 2, marshals)
+
+ // So does runtime/types, which cannot import this package.
+ d := types.Date{Time: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC)}
+ got, err = d.MarshalJSON()
+ require.NoError(t, err)
+ assert.Equal(t, `"2020-01-02"`, string(got))
+ assert.Equal(t, 3, marshals)
+ require.NoError(t, d.UnmarshalJSON(got))
+ assert.Equal(t, 2, unmarshals)
+
+ // nil restores the default.
+ SetJSONCodec(nil)
+ got, err = JSONMarshal(in)
+ require.NoError(t, err)
+ assert.Equal(t, string(escaped), string(got))
+ assert.Equal(t, 3, marshals)
+}
+
+func TestJSONCodec_Streams(t *testing.T) {
+ t.Cleanup(func() { SetJSONCodec(nil) })
+
+ const in = ""
+
+ // encoded is in as the default encodes a body: HTML escaped, trailing newline.
+ var encoded bytes.Buffer
+ require.NoError(t, json.NewEncoder(&encoded).Encode(in))
+
+ t.Run("default is encoding/json's Encoder and Decoder", func(t *testing.T) {
+ var buf bytes.Buffer
+ require.NoError(t, JSONMarshalWrite(&buf, in))
+ assert.Equal(t, encoded.String(), buf.String())
+
+ var out string
+ require.NoError(t, JSONUnmarshalRead(strings.NewReader(`"x"`), &out))
+ assert.Equal(t, "x", out)
+ assert.ErrorIs(t, JSONUnmarshalRead(strings.NewReader(""), &out), io.EOF)
+ })
+
+ t.Run("StdJSONCodec is the default too", func(t *testing.T) {
+ SetJSONCodec(StdJSONCodec{})
+ var buf bytes.Buffer
+ require.NoError(t, JSONMarshalWrite(&buf, in))
+ assert.Equal(t, encoded.String(), buf.String(), "not the buffered path, which has no newline")
+ })
+
+ t.Run("StdJSONCodec itself doesn't stream", func(t *testing.T) {
+ _, ok := any(StdJSONCodec{}).(StreamingJSONCodec)
+ assert.False(t, ok, "embedding it must not promote stream methods past an overridden Marshal")
+ })
+
+ t.Run("a codec without stream methods is buffered through them", func(t *testing.T) {
+ var marshals, unmarshals int
+ SetJSONCodec(noEscapeCodec{marshals: &marshals, unmarshals: &unmarshals})
+
+ var buf bytes.Buffer
+ require.NoError(t, JSONMarshalWrite(&buf, in))
+ assert.Equal(t, `""`, buf.String(), "the overridden Marshal, not encoding/json's Encoder")
+ assert.Equal(t, 1, marshals)
+
+ var out string
+ require.NoError(t, JSONUnmarshalRead(strings.NewReader(`"x"`), &out))
+ assert.Equal(t, "x", out)
+ assert.Equal(t, 1, unmarshals)
+ })
+
+ t.Run("a codec with stream methods streams", func(t *testing.T) {
+ var marshals, unmarshals, writes, reads int
+ SetJSONCodec(streamingCodec{
+ noEscapeCodec: noEscapeCodec{marshals: &marshals, unmarshals: &unmarshals},
+ writes: &writes,
+ reads: &reads,
+ })
+
+ var buf bytes.Buffer
+ require.NoError(t, JSONMarshalWrite(&buf, in))
+ assert.Equal(t, "streamed", buf.String())
+
+ var out string
+ require.NoError(t, JSONUnmarshalRead(strings.NewReader(`"x"`), &out))
+ assert.Equal(t, "streamed", out)
+
+ assert.Equal(t, 1, writes)
+ assert.Equal(t, 1, reads)
+ assert.Equal(t, 0, marshals, "the byte methods are not used for streams")
+ assert.Equal(t, 0, unmarshals)
+ })
+}
+
+func TestJSONUnmarshalRead_EmptyInput(t *testing.T) {
+ t.Cleanup(func() { SetJSONCodec(nil) })
+
+ t.Run("no value is io.EOF whichever codec is in use", func(t *testing.T) {
+ var unmarshals, reads int
+ for _, codec := range []JSONCodec{
+ nil,
+ noEscapeCodec{marshals: new(int), unmarshals: &unmarshals},
+ streamingCodec{noEscapeCodec: noEscapeCodec{marshals: new(int), unmarshals: &unmarshals}, writes: new(int), reads: &reads},
+ } {
+ SetJSONCodec(codec)
+ var out string
+ assert.ErrorIs(t, JSONUnmarshalRead(nil, &out), io.EOF)
+ assert.ErrorIs(t, JSONUnmarshalRead(strings.NewReader(""), &out), io.EOF)
+ assert.ErrorIs(t, JSONUnmarshalRead(strings.NewReader(" \r\n\t"), &out), io.EOF)
+ }
+ assert.Equal(t, 0, unmarshals, "the codec never saw the empty inputs")
+ assert.Equal(t, 0, reads)
+ })
+
+ t.Run("a read error is returned", func(t *testing.T) {
+ SetJSONCodec(nil)
+ readErr := errors.New("read failed")
+ var out string
+ assert.ErrorIs(t, JSONUnmarshalRead(errReader{readErr}, &out), readErr)
+ })
+
+ t.Run("the input reaches the codec unchanged", func(t *testing.T) {
+ SetJSONCodec(nil)
+ const in = " {x"
+ var want, got map[string]any
+ wantErr := json.NewDecoder(strings.NewReader(in)).Decode(&want)
+ gotErr := JSONUnmarshalRead(strings.NewReader(in), &got)
+ var wantSyntax, gotSyntax *json.SyntaxError
+ require.ErrorAs(t, wantErr, &wantSyntax)
+ require.ErrorAs(t, gotErr, &gotSyntax)
+ assert.Equal(t, wantSyntax.Offset, gotSyntax.Offset, "leading white space is replayed, not dropped")
+ assert.Equal(t, wantErr.Error(), gotErr.Error())
+
+ require.NoError(t, JSONUnmarshalRead(strings.NewReader("\n\t {\"a\":1}"), &got))
+ assert.Equal(t, map[string]any{"a": 1.0}, got)
+ })
+}
+
+func TestBindJSONBody_Codec(t *testing.T) {
+ t.Cleanup(func() { SetJSONCodec(nil) })
+
+ newRequest := func(body string) *http.Request {
+ r, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(body))
+ require.NoError(t, err)
+ return r
+ }
+
+ var marshals, unmarshals int
+ SetJSONCodec(noEscapeCodec{marshals: &marshals, unmarshals: &unmarshals})
+
+ t.Run("decodes through the codec", func(t *testing.T) {
+ var got map[string]string
+ require.NoError(t, BindJSONBody(newRequest(` {"name":"bob"}`), &got))
+ assert.Equal(t, map[string]string{"name": "bob"}, got)
+ assert.Equal(t, 1, unmarshals)
+ })
+
+ t.Run("an empty body is io.EOF whichever codec is in use", func(t *testing.T) {
+ var got map[string]string
+ assert.ErrorIs(t, BindJSONBody(newRequest(""), &got), io.EOF)
+ assert.ErrorIs(t, BindJSONBody(newRequest(" \r\n\t"), &got), io.EOF)
+ assert.ErrorIs(t, BindJSONBody(&http.Request{}, &got), io.EOF)
+ assert.ErrorIs(t, BindJSONBody(nil, &got), io.EOF)
+ assert.Equal(t, 1, unmarshals, "the codec never saw the empty bodies")
+ })
+}
diff --git a/types/date.go b/types/date.go
index 393dbda..a2eebad 100644
--- a/types/date.go
+++ b/types/date.go
@@ -1,8 +1,9 @@
package types
import (
- "encoding/json"
"time"
+
+ "github.com/oapi-codegen/runtime/internal/jsoncodec"
)
const DateFormat = "2006-01-02"
@@ -12,12 +13,12 @@ type Date struct {
}
func (d Date) MarshalJSON() ([]byte, error) {
- return json.Marshal(d.Format(DateFormat))
+ return jsoncodec.Marshal(d.Format(DateFormat))
}
func (d *Date) UnmarshalJSON(data []byte) error {
var dateStr string
- err := json.Unmarshal(data, &dateStr)
+ err := jsoncodec.Unmarshal(data, &dateStr)
if err != nil {
return err
}
diff --git a/types/duration.go b/types/duration.go
index 2ef80c8..01803a9 100644
--- a/types/duration.go
+++ b/types/duration.go
@@ -1,12 +1,13 @@
package types
import (
- "encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
+
+ "github.com/oapi-codegen/runtime/internal/jsoncodec"
)
// Duration represents an RFC 3339 duration (the ISO 8601 duration grammar
@@ -241,12 +242,12 @@ func FromTimeDuration(td time.Duration) (Duration, error) {
}
func (d Duration) MarshalJSON() ([]byte, error) {
- return json.Marshal(d.String())
+ return jsoncodec.Marshal(d.String())
}
func (d *Duration) UnmarshalJSON(data []byte) error {
var s string
- if err := json.Unmarshal(data, &s); err != nil {
+ if err := jsoncodec.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := ParseDuration(s)
diff --git a/types/email.go b/types/email.go
index 234e7cc..e0ca71f 100644
--- a/types/email.go
+++ b/types/email.go
@@ -1,9 +1,10 @@
package types
import (
- "encoding/json"
"errors"
"net/mail"
+
+ "github.com/oapi-codegen/runtime/internal/jsoncodec"
)
// ErrValidationEmail is the sentinel error returned when an email fails validation
@@ -20,7 +21,7 @@ func (e Email) MarshalJSON() ([]byte, error) {
return nil, ErrValidationEmail
}
- return json.Marshal(m.Address)
+ return jsoncodec.Marshal(m.Address)
}
func (e *Email) UnmarshalJSON(data []byte) error {
@@ -29,7 +30,7 @@ func (e *Email) UnmarshalJSON(data []byte) error {
}
var s string
- if err := json.Unmarshal(data, &s); err != nil {
+ if err := jsoncodec.Unmarshal(data, &s); err != nil {
return err
}
diff --git a/types/file.go b/types/file.go
index 7b26a0d..1b06687 100644
--- a/types/file.go
+++ b/types/file.go
@@ -2,9 +2,10 @@ package types
import (
"bytes"
- "encoding/json"
"io"
"mime/multipart"
+
+ "github.com/oapi-codegen/runtime/internal/jsoncodec"
)
type File struct {
@@ -30,11 +31,11 @@ func (file File) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
- return json.Marshal(b)
+ return jsoncodec.Marshal(b)
}
func (file *File) UnmarshalJSON(data []byte) error {
- return json.Unmarshal(data, &file.data)
+ return jsoncodec.Unmarshal(data, &file.data)
}
func (file File) Bytes() ([]byte, error) {