Skip to content

Commit

Permalink
feat: support custom json codec at runtime
Browse files Browse the repository at this point in the history
Add custom json codec examples in readme file
  • Loading branch information
timandy committed Nov 10, 2022
1 parent 31bc4c0 commit 5e99ff5
Show file tree
Hide file tree
Showing 15 changed files with 416 additions and 69 deletions.
4 changes: 2 additions & 2 deletions binding/form_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case time.Time:
return setTimeField(val, field, value)
}
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.Api.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
return json.Api.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
default:
return errUnknownType
}
Expand Down
2 changes: 1 addition & 1 deletion binding/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
}

func decodeJSON(r io.Reader, obj any) error {
decoder := json.NewDecoder(r)
decoder := json.Api.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}
Expand Down
190 changes: 190 additions & 0 deletions binding/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@
package binding

import (
"io"
"net/http/httptest"
"testing"
"time"
"unsafe"

"github.com/gin-gonic/gin/codec/api"
"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/require"
)
Expand All @@ -28,3 +37,184 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "FOO", s["foo"])
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)
assert.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) api.JsonEncoder {
return customConfig.NewEncoder(writer)
}

func (j customJsonApi) NewDecoder(reader io.Reader) api.JsonDecoder {
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
10 changes: 10 additions & 0 deletions codec/api/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

//go:build !go1.18
// +build !go1.18

package api

type any = interface{}
54 changes: 54 additions & 0 deletions codec/api/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package api

import "io"

// JsonApi the api for json codec.
type JsonApi interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
MarshalIndent(v any, prefix, indent string) ([]byte, error)
NewEncoder(writer io.Writer) JsonEncoder
NewDecoder(reader io.Reader) JsonDecoder
}

// A JsonEncoder interface writes JSON values to an output stream.
type JsonEncoder 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
}

// A JsonDecoder interface reads and decodes JSON values from an input stream.
type JsonDecoder 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
}
10 changes: 10 additions & 0 deletions codec/json/any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

//go:build !go1.18
// +build !go1.18

package json

type any = interface{}
5 changes: 5 additions & 0 deletions codec/json/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package json

import "github.com/gin-gonic/gin/codec/api"

var Api api.JsonApi
45 changes: 32 additions & 13 deletions codec/json/go_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,36 @@

package json

import json "github.com/goccy/go-json"

var (
// 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
import (
"io"

"github.com/gin-gonic/gin/codec/api"
"github.com/goccy/go-json"
)

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) api.JsonEncoder {
return json.NewEncoder(writer)
}

func (j gojsonApi) NewDecoder(reader io.Reader) api.JsonDecoder {
return json.NewDecoder(reader)
}
Loading

0 comments on commit 5e99ff5

Please sign in to comment.