feat(json): support custom json codec at runtime

This commit is contained in:
TimAndy 2025-05-21 10:46:24 +08:00
parent 07167886be
commit c5bd5f7702
13 changed files with 383 additions and 68 deletions

View File

@ -333,9 +333,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case multipart.FileHeader: case multipart.FileHeader:
return nil return nil
} }
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map: case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Ptr: case reflect.Ptr:
if !value.Elem().IsValid() { if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem())) value.Set(reflect.New(value.Type().Elem()))

View File

@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
} }
func decodeJSON(r io.Reader, obj any) error { func decodeJSON(r io.Reader, obj any) error {
decoder := json.NewDecoder(r) decoder := json.API.NewDecoder(r)
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }

View File

@ -5,8 +5,16 @@
package binding package binding
import ( import (
"io"
"net/http/httptest"
"testing" "testing"
"time"
"unsafe"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/render"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -28,3 +36,184 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "FOO", s["foo"]) assert.Equal(t, "FOO", s["foo"])
assert.Equal(t, "world", s["hello"]) assert.Equal(t, "world", s["hello"])
} }
func TestCustomJsonCodec(t *testing.T) {
//Restore json encoding configuration after testing
oldMarshal := json.API
defer func() {
json.API = oldMarshal
}()
//Custom json api
json.API = customJsonApi{}
//test decode json
obj := customReq{}
err := jsonBinding{}.BindBody([]byte(`{"time_empty":null,"time_struct": "2001-12-05 10:01:02.345","time_nil":null,"time_pointer":"2002-12-05 10:01:02.345"}`), &obj)
require.NoError(t, err)
assert.Equal(t, zeroTime, obj.TimeEmpty)
assert.Equal(t, time.Date(2001, 12, 05, 10, 01, 02, 345000000, time.Local), obj.TimeStruct)
assert.Nil(t, obj.TimeNil)
assert.Equal(t, time.Date(2002, 12, 05, 10, 01, 02, 345000000, time.Local), *obj.TimePointer)
//test encode json
w := httptest.NewRecorder()
err2 := (render.PureJSON{Data: obj}).Render(w)
require.NoError(t, err2)
assert.Equal(t, "{\"time_empty\":null,\"time_struct\":\"2001-12-05 10:01:02.345\",\"time_nil\":null,\"time_pointer\":\"2002-12-05 10:01:02.345\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
type customReq struct {
TimeEmpty time.Time `json:"time_empty"`
TimeStruct time.Time `json:"time_struct"`
TimeNil *time.Time `json:"time_nil"`
TimePointer *time.Time `json:"time_pointer"`
}
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
func init() {
customConfig.RegisterExtension(&TimeEx{})
customConfig.RegisterExtension(&TimePointerEx{})
}
type customJsonApi struct {
}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
//region Time Extension
var (
zeroTime = time.Time{}
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
defaultTimeCodec = &timeCodec{}
)
type TimeEx struct {
jsoniter.DummyExtension
}
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timeType {
return defaultTimeCodec
}
return nil
}
type timeCodec struct {
}
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((*time.Time)(ptr))
return t == zeroTime
}
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((*time.Time)(ptr))
if t == zeroTime {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((*time.Time)(ptr)) = zeroTime
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((*time.Time)(ptr)) = t
}
//endregion
//region *Time Extension
var (
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
defaultTimePointerCodec = &timePointerCodec{}
)
type TimePointerEx struct {
jsoniter.DummyExtension
}
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
if typ == timePointerType {
return defaultTimePointerCodec
}
return nil
}
type timePointerCodec struct {
}
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
t := *((**time.Time)(ptr))
return t == nil || *t == zeroTime
}
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
t := *((**time.Time)(ptr))
if t == nil || *t == zeroTime {
stream.WriteNil()
return
}
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
}
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
ts := iter.ReadString()
if len(ts) == 0 {
*((**time.Time)(ptr)) = nil
return
}
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
if err != nil {
panic(err)
}
*((**time.Time)(ptr)) = &t
}
//endregion

53
codec/json/api.go Normal file
View File

@ -0,0 +1,53 @@
package json
import "io"
// API the json codec in use.
var API Core
// Core the api for json codec.
type Core interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
MarshalIndent(v any, prefix, indent string) ([]byte, error)
NewEncoder(writer io.Writer) Encoder
NewDecoder(reader io.Reader) Decoder
}
// Encoder an interface writes JSON values to an output stream.
type Encoder interface {
// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
SetEscapeHTML(on bool)
// Encode writes the JSON encoding of v to the stream,
// followed by a newline character.
//
// See the documentation for Marshal for details about the
// conversion of Go values to JSON.
Encode(v interface{}) error
}
// Decoder an interface reads and decodes JSON values from an input stream.
type Decoder interface {
// UseNumber causes the Decoder to unmarshal a number into an interface{} as a
// Number instead of as a float64.
UseNumber()
// DisallowUnknownFields causes the Decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
DisallowUnknownFields()
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
//
// See the documentation for Unmarshal for details about
// the conversion of JSON into a Go value.
Decode(v interface{}) error
}

View File

@ -6,17 +6,35 @@
package json package json
import json "github.com/goccy/go-json" import (
"io"
var ( "github.com/goccy/go-json"
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )
func init() {
API = gojsonApi{}
}
type gojsonApi struct {
}
func (j gojsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j gojsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -6,17 +6,34 @@
package json package json
import "encoding/json" import (
"encoding/json"
var ( "io"
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )
func init() {
API = jsonApi{}
}
type jsonApi struct {
}
func (j jsonApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsonApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -6,18 +6,37 @@
package json package json
import jsoniter "github.com/json-iterator/go" import (
"io"
var ( jsoniter "github.com/json-iterator/go"
json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )
func init() {
API = jsoniterApi{}
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type jsoniterApi struct {
}
func (j jsoniterApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j jsoniterApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j jsoniterApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j jsoniterApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j jsoniterApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -6,18 +6,37 @@
package json package json
import "github.com/bytedance/sonic" import (
"io"
var ( "github.com/bytedance/sonic"
json = sonic.ConfigStd
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
) )
func init() {
API = sonicApi{}
}
var json = sonic.ConfigStd
type sonicApi struct {
}
func (j sonicApi) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (j sonicApi) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (j sonicApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return json.MarshalIndent(v, prefix, indent)
}
func (j sonicApi) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (j sonicApi) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}

View File

@ -77,7 +77,7 @@ func (msg *Error) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (msg *Error) MarshalJSON() ([]byte, error) { func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON()) return json.API.Marshal(msg.JSON())
} }
// Error implements the error interface. // Error implements the error interface.
@ -157,7 +157,7 @@ func (a errorMsgs) JSON() any {
// MarshalJSON implements the json.Marshaller interface. // MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) { func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON()) return json.API.Marshal(a.JSON())
} }
func (a errorMsgs) String() string { func (a errorMsgs) String() string {

View File

@ -33,7 +33,7 @@ func TestError(t *testing.T) {
"meta": "some data", "meta": "some data",
}, err.JSON()) }, err.JSON())
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.API.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ //nolint: errcheck err.SetMeta(H{ //nolint: errcheck
@ -92,13 +92,13 @@ Error #03: third
H{"error": "second", "meta": "some data"}, H{"error": "second", "meta": "some data"},
H{"error": "third", "status": "400"}, H{"error": "third", "status": "400"},
}, errs.JSON()) }, errs.JSON())
jsonBytes, _ := json.Marshal(errs) jsonBytes, _ := json.API.Marshal(errs)
assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes)) assert.Equal(t, "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]", string(jsonBytes))
errs = errorMsgs{ errs = errorMsgs{
{Err: errors.New("first"), Type: ErrorTypePrivate}, {Err: errors.New("first"), Type: ErrorTypePrivate},
} }
assert.Equal(t, H{"error": "first"}, errs.JSON()) assert.Equal(t, H{"error": "first"}, errs.JSON())
jsonBytes, _ = json.Marshal(errs) jsonBytes, _ = json.API.Marshal(errs)
assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"first\"}", string(jsonBytes))
errs = errorMsgs{} errs = errorMsgs{}

2
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/quic-go/quic-go v0.51.0 github.com/quic-go/quic-go v0.51.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
@ -30,7 +31,6 @@ require (
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect

View File

@ -66,7 +66,7 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
// WriteJSON marshals the given interface object and writes it with custom ContentType. // WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj any) error { func WriteJSON(w http.ResponseWriter, obj any) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj) jsonBytes, err := json.API.Marshal(obj)
if err != nil { if err != nil {
return err return err
} }
@ -77,7 +77,7 @@ func WriteJSON(w http.ResponseWriter, obj any) error {
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType. // Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ") jsonBytes, err := json.API.MarshalIndent(r.Data, "", " ")
if err != nil { if err != nil {
return err return err
} }
@ -93,7 +93,7 @@ func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType. // Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error { func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data) jsonBytes, err := json.API.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
@ -116,7 +116,7 @@ func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType. // Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) { func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.Marshal(r.Data) ret, err := json.API.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
@ -154,7 +154,7 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType. // Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) error { func (r AsciiJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
ret, err := json.Marshal(r.Data) ret, err := json.API.Marshal(r.Data)
if err != nil { if err != nil {
return err return err
} }
@ -183,7 +183,7 @@ func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
// Render (PureJSON) writes custom ContentType and encodes the given interface object. // Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error { func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
encoder := json.NewEncoder(w) encoder := json.API.NewEncoder(w)
encoder.SetEscapeHTML(false) encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data) return encoder.Encode(r.Data)
} }

View File

@ -15,7 +15,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/codec/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -173,7 +173,7 @@ func TestRenderJsonpJSONError(t *testing.T) {
err = jsonpJSON.Render(ew) err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+`(`+`" error`, err.Error()) assert.Equal(t, `write "`+`(`+`" error`, err.Error())
data, _ := json.Marshal(jsonpJSON.Data) // error was returned while writing data data, _ := json.API.Marshal(jsonpJSON.Data) // error was returned while writing data
ew.bufString = string(data) ew.bufString = string(data)
err = jsonpJSON.Render(ew) err = jsonpJSON.Render(ew)
assert.Equal(t, `write "`+string(data)+`" error`, err.Error()) assert.Equal(t, `write "`+string(data)+`" error`, err.Error())