Skip to content

Commit 688a429

Browse files
authored
feat: support custom json codec at runtime (#3391)
* refactor(json): export json codec * feat(json): support custom json codec at runtime * chore(copyright): update copyright to 2025 gin core team * docs(gin): add custom json codec examples in doc file
1 parent 0a86488 commit 688a429

File tree

19 files changed

+497
-125
lines changed

19 files changed

+497
-125
lines changed

binding/form_mapping.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
"strings"
1414
"time"
1515

16+
"github.com/gin-gonic/gin/codec/json"
1617
"github.com/gin-gonic/gin/internal/bytesconv"
17-
"github.com/gin-gonic/gin/internal/json"
1818
)
1919

2020
var (
@@ -333,9 +333,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
333333
case multipart.FileHeader:
334334
return nil
335335
}
336-
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
336+
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
337337
case reflect.Map:
338-
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
338+
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
339339
case reflect.Ptr:
340340
if !value.Elem().IsValid() {
341341
value.Set(reflect.New(value.Type().Elem()))

binding/json.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"io"
1111
"net/http"
1212

13-
"github.com/gin-gonic/gin/internal/json"
13+
"github.com/gin-gonic/gin/codec/json"
1414
)
1515

1616
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@@ -42,7 +42,7 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
4242
}
4343

4444
func decodeJSON(r io.Reader, obj any) error {
45-
decoder := json.NewDecoder(r)
45+
decoder := json.API.NewDecoder(r)
4646
if EnableDecoderUseNumber {
4747
decoder.UseNumber()
4848
}

binding/json_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@
55
package binding
66

77
import (
8+
"io"
9+
"net/http/httptest"
810
"testing"
11+
"time"
12+
"unsafe"
913

14+
"github.com/gin-gonic/gin/codec/json"
15+
"github.com/gin-gonic/gin/render"
16+
jsoniter "github.com/json-iterator/go"
17+
"github.com/modern-go/reflect2"
1018
"github.com/stretchr/testify/assert"
1119
"github.com/stretchr/testify/require"
1220
)
@@ -28,3 +36,181 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
2836
assert.Equal(t, "FOO", s["foo"])
2937
assert.Equal(t, "world", s["hello"])
3038
}
39+
40+
func TestCustomJsonCodec(t *testing.T) {
41+
// Restore json encoding configuration after testing
42+
oldMarshal := json.API
43+
defer func() {
44+
json.API = oldMarshal
45+
}()
46+
// Custom json api
47+
json.API = customJsonApi{}
48+
49+
// test decode json
50+
obj := customReq{}
51+
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)
52+
require.NoError(t, err)
53+
assert.Equal(t, zeroTime, obj.TimeEmpty)
54+
assert.Equal(t, time.Date(2001, 12, 5, 10, 1, 2, 345000000, time.Local), obj.TimeStruct)
55+
assert.Nil(t, obj.TimeNil)
56+
assert.Equal(t, time.Date(2002, 12, 5, 10, 1, 2, 345000000, time.Local), *obj.TimePointer)
57+
// test encode json
58+
w := httptest.NewRecorder()
59+
err2 := (render.PureJSON{Data: obj}).Render(w)
60+
require.NoError(t, err2)
61+
assert.JSONEq(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())
62+
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
63+
}
64+
65+
type customReq struct {
66+
TimeEmpty time.Time `json:"time_empty"`
67+
TimeStruct time.Time `json:"time_struct"`
68+
TimeNil *time.Time `json:"time_nil"`
69+
TimePointer *time.Time `json:"time_pointer"`
70+
}
71+
72+
var customConfig = jsoniter.Config{
73+
EscapeHTML: true,
74+
SortMapKeys: true,
75+
ValidateJsonRawMessage: true,
76+
}.Froze()
77+
78+
func init() {
79+
customConfig.RegisterExtension(&TimeEx{})
80+
customConfig.RegisterExtension(&TimePointerEx{})
81+
}
82+
83+
type customJsonApi struct{}
84+
85+
func (j customJsonApi) Marshal(v any) ([]byte, error) {
86+
return customConfig.Marshal(v)
87+
}
88+
89+
func (j customJsonApi) Unmarshal(data []byte, v any) error {
90+
return customConfig.Unmarshal(data, v)
91+
}
92+
93+
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
94+
return customConfig.MarshalIndent(v, prefix, indent)
95+
}
96+
97+
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
98+
return customConfig.NewEncoder(writer)
99+
}
100+
101+
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
102+
return customConfig.NewDecoder(reader)
103+
}
104+
105+
// region Time Extension
106+
107+
var (
108+
zeroTime = time.Time{}
109+
timeType = reflect2.TypeOfPtr((*time.Time)(nil)).Elem()
110+
defaultTimeCodec = &timeCodec{}
111+
)
112+
113+
type TimeEx struct {
114+
jsoniter.DummyExtension
115+
}
116+
117+
func (te *TimeEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
118+
if typ == timeType {
119+
return defaultTimeCodec
120+
}
121+
return nil
122+
}
123+
124+
func (te *TimeEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
125+
if typ == timeType {
126+
return defaultTimeCodec
127+
}
128+
return nil
129+
}
130+
131+
type timeCodec struct{}
132+
133+
func (tc timeCodec) IsEmpty(ptr unsafe.Pointer) bool {
134+
t := *((*time.Time)(ptr))
135+
return t.Equal(zeroTime)
136+
}
137+
138+
func (tc timeCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
139+
t := *((*time.Time)(ptr))
140+
if t.Equal(zeroTime) {
141+
stream.WriteNil()
142+
return
143+
}
144+
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
145+
}
146+
147+
func (tc timeCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
148+
ts := iter.ReadString()
149+
if len(ts) == 0 {
150+
*((*time.Time)(ptr)) = zeroTime
151+
return
152+
}
153+
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
154+
if err != nil {
155+
panic(err)
156+
}
157+
*((*time.Time)(ptr)) = t
158+
}
159+
160+
// endregion
161+
162+
// region *Time Extension
163+
164+
var (
165+
timePointerType = reflect2.TypeOfPtr((**time.Time)(nil)).Elem()
166+
defaultTimePointerCodec = &timePointerCodec{}
167+
)
168+
169+
type TimePointerEx struct {
170+
jsoniter.DummyExtension
171+
}
172+
173+
func (tpe *TimePointerEx) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {
174+
if typ == timePointerType {
175+
return defaultTimePointerCodec
176+
}
177+
return nil
178+
}
179+
180+
func (tpe *TimePointerEx) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {
181+
if typ == timePointerType {
182+
return defaultTimePointerCodec
183+
}
184+
return nil
185+
}
186+
187+
type timePointerCodec struct{}
188+
189+
func (tpc timePointerCodec) IsEmpty(ptr unsafe.Pointer) bool {
190+
t := *((**time.Time)(ptr))
191+
return t == nil || (*t).Equal(zeroTime)
192+
}
193+
194+
func (tpc timePointerCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
195+
t := *((**time.Time)(ptr))
196+
if t == nil || (*t).Equal(zeroTime) {
197+
stream.WriteNil()
198+
return
199+
}
200+
stream.WriteString(t.In(time.Local).Format("2006-01-02 15:04:05.000"))
201+
}
202+
203+
func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
204+
ts := iter.ReadString()
205+
if len(ts) == 0 {
206+
*((**time.Time)(ptr)) = nil
207+
return
208+
}
209+
t, err := time.ParseInLocation("2006-01-02 15:04:05.000", ts, time.Local)
210+
if err != nil {
211+
panic(err)
212+
}
213+
*((**time.Time)(ptr)) = &t
214+
}
215+
216+
// endregion

codec/json/api.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package json
6+
7+
import "io"
8+
9+
// API the json codec in use.
10+
var API Core
11+
12+
// Core the api for json codec.
13+
type Core interface {
14+
Marshal(v any) ([]byte, error)
15+
Unmarshal(data []byte, v any) error
16+
MarshalIndent(v any, prefix, indent string) ([]byte, error)
17+
NewEncoder(writer io.Writer) Encoder
18+
NewDecoder(reader io.Reader) Decoder
19+
}
20+
21+
// Encoder an interface writes JSON values to an output stream.
22+
type Encoder interface {
23+
// SetEscapeHTML specifies whether problematic HTML characters
24+
// should be escaped inside JSON quoted strings.
25+
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
26+
// to avoid certain safety problems that can arise when embedding JSON in HTML.
27+
//
28+
// In non-HTML settings where the escaping interferes with the readability
29+
// of the output, SetEscapeHTML(false) disables this behavior.
30+
SetEscapeHTML(on bool)
31+
32+
// Encode writes the JSON encoding of v to the stream,
33+
// followed by a newline character.
34+
//
35+
// See the documentation for Marshal for details about the
36+
// conversion of Go values to JSON.
37+
Encode(v any) error
38+
}
39+
40+
// Decoder an interface reads and decodes JSON values from an input stream.
41+
type Decoder interface {
42+
// UseNumber causes the Decoder to unmarshal a number into an any as a
43+
// Number instead of as a float64.
44+
UseNumber()
45+
46+
// DisallowUnknownFields causes the Decoder to return an error when the destination
47+
// is a struct and the input contains object keys which do not match any
48+
// non-ignored, exported fields in the destination.
49+
DisallowUnknownFields()
50+
51+
// Decode reads the next JSON-encoded value from its
52+
// input and stores it in the value pointed to by v.
53+
//
54+
// See the documentation for Unmarshal for details about
55+
// the conversion of JSON into a Go value.
56+
Decode(v any) error
57+
}

codec/json/go_json.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go_json
6+
7+
package json
8+
9+
import (
10+
"io"
11+
12+
"github.com/goccy/go-json"
13+
)
14+
15+
// Package indicates what library is being used for JSON encoding.
16+
const Package = "github.com/goccy/go-json"
17+
18+
func init() {
19+
API = gojsonApi{}
20+
}
21+
22+
type gojsonApi struct{}
23+
24+
func (j gojsonApi) Marshal(v any) ([]byte, error) {
25+
return json.Marshal(v)
26+
}
27+
28+
func (j gojsonApi) Unmarshal(data []byte, v any) error {
29+
return json.Unmarshal(data, v)
30+
}
31+
32+
func (j gojsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
33+
return json.MarshalIndent(v, prefix, indent)
34+
}
35+
36+
func (j gojsonApi) NewEncoder(writer io.Writer) Encoder {
37+
return json.NewEncoder(writer)
38+
}
39+
40+
func (j gojsonApi) NewDecoder(reader io.Reader) Decoder {
41+
return json.NewDecoder(reader)
42+
}

codec/json/json.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2025 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !jsoniter && !go_json && !(sonic && (linux || windows || darwin))
6+
7+
package json
8+
9+
import (
10+
"encoding/json"
11+
"io"
12+
)
13+
14+
// Package indicates what library is being used for JSON encoding.
15+
const Package = "encoding/json"
16+
17+
func init() {
18+
API = jsonApi{}
19+
}
20+
21+
type jsonApi struct{}
22+
23+
func (j jsonApi) Marshal(v any) ([]byte, error) {
24+
return json.Marshal(v)
25+
}
26+
27+
func (j jsonApi) Unmarshal(data []byte, v any) error {
28+
return json.Unmarshal(data, v)
29+
}
30+
31+
func (j jsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
32+
return json.MarshalIndent(v, prefix, indent)
33+
}
34+
35+
func (j jsonApi) NewEncoder(writer io.Writer) Encoder {
36+
return json.NewEncoder(writer)
37+
}
38+
39+
func (j jsonApi) NewDecoder(reader io.Reader) Decoder {
40+
return json.NewDecoder(reader)
41+
}

0 commit comments

Comments
 (0)