Skip to content

Commit d150248

Browse files
authored
chore: refactor jsonx.Marshal (#5045)
Signed-off-by: Kevin Wan <[email protected]>
1 parent 610a734 commit d150248

File tree

4 files changed

+123
-127
lines changed

4 files changed

+123
-127
lines changed

core/jsonx/json.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,25 @@ import (
88
"strings"
99
)
1010

11-
// Marshal marshals v into json bytes.
11+
// Marshal marshals v into json bytes, without escaping HTML and removes the trailing newline.
1212
func Marshal(v any) ([]byte, error) {
13-
return json.Marshal(v)
13+
// why not use json.Marshal? https://github.com/golang/go/issues/28453
14+
// it changes the behavior of json.Marshal, like & -> \u0026, < -> \u003c, > -> \u003e
15+
// which is not what we want in API responses
16+
var buf bytes.Buffer
17+
enc := json.NewEncoder(&buf)
18+
enc.SetEscapeHTML(false)
19+
if err := enc.Encode(v); err != nil {
20+
return nil, err
21+
}
22+
23+
bs := buf.Bytes()
24+
// Remove trailing newline added by json.Encoder.Encode
25+
if len(bs) > 0 && bs[len(bs)-1] == '\n' {
26+
bs = bs[:len(bs)-1]
27+
}
28+
29+
return bs, nil
1430
}
1531

1632
// MarshalToString marshals v into a string.

core/jsonx/json_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package jsonx
22

33
import (
4+
"fmt"
45
"strings"
56
"testing"
67

@@ -101,3 +102,105 @@ func TestUnmarshalFromReaderError(t *testing.T) {
101102
err := UnmarshalFromReader(strings.NewReader(s), &v)
102103
assert.NotNil(t, err)
103104
}
105+
106+
func Test_doMarshalJson(t *testing.T) {
107+
type args struct {
108+
v any
109+
}
110+
111+
tests := []struct {
112+
name string
113+
args args
114+
want []byte
115+
wantErr assert.ErrorAssertionFunc
116+
}{
117+
{
118+
name: "nil",
119+
args: args{nil},
120+
want: []byte("null"),
121+
wantErr: assert.NoError,
122+
},
123+
{
124+
name: "string",
125+
args: args{"hello"},
126+
want: []byte(`"hello"`),
127+
wantErr: assert.NoError,
128+
},
129+
{
130+
name: "int",
131+
args: args{42},
132+
want: []byte("42"),
133+
wantErr: assert.NoError,
134+
},
135+
{
136+
name: "bool",
137+
args: args{true},
138+
want: []byte("true"),
139+
wantErr: assert.NoError,
140+
},
141+
{
142+
name: "struct",
143+
args: args{
144+
struct {
145+
Name string `json:"name"`
146+
}{Name: "test"},
147+
},
148+
want: []byte(`{"name":"test"}`),
149+
wantErr: assert.NoError,
150+
},
151+
{
152+
name: "slice",
153+
args: args{[]int{1, 2, 3}},
154+
want: []byte("[1,2,3]"),
155+
wantErr: assert.NoError,
156+
},
157+
{
158+
name: "map",
159+
args: args{map[string]int{"a": 1, "b": 2}},
160+
want: []byte(`{"a":1,"b":2}`),
161+
wantErr: assert.NoError,
162+
},
163+
{
164+
name: "unmarshalable type",
165+
args: args{complex(1, 2)},
166+
want: nil,
167+
wantErr: assert.Error,
168+
},
169+
{
170+
name: "channel type",
171+
args: args{make(chan int)},
172+
want: nil,
173+
wantErr: assert.Error,
174+
},
175+
{
176+
name: "url with query params",
177+
args: args{"https://example.com/api?name=test&age=25"},
178+
want: []byte(`"https://example.com/api?name=test&age=25"`),
179+
wantErr: assert.NoError,
180+
},
181+
{
182+
name: "url with encoded query params",
183+
args: args{"https://example.com/api?data=hello%20world&special=%26%3D"},
184+
want: []byte(`"https://example.com/api?data=hello%20world&special=%26%3D"`),
185+
wantErr: assert.NoError,
186+
},
187+
{
188+
name: "url with multiple query params",
189+
args: args{"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"},
190+
want: []byte(`"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"`),
191+
wantErr: assert.NoError,
192+
},
193+
}
194+
195+
for _, tt := range tests {
196+
tt := tt
197+
t.Run(tt.name, func(t *testing.T) {
198+
got, err := Marshal(tt.args.v)
199+
if !tt.wantErr(t, err, fmt.Sprintf("Marshal(%v)", tt.args.v)) {
200+
return
201+
}
202+
203+
assert.Equalf(t, string(tt.want), string(got), "Marshal(%v)", tt.args.v)
204+
})
205+
}
206+
}

rest/httpx/responses.go

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package httpx
22

33
import (
4-
"bytes"
54
"context"
6-
"encoding/json"
75
"errors"
86
"fmt"
97
"io"
108
"net/http"
119
"sync"
1210

11+
"github.com/zeromicro/go-zero/core/jsonx"
1312
"github.com/zeromicro/go-zero/core/logc"
1413
"github.com/zeromicro/go-zero/core/logx"
1514
"github.com/zeromicro/go-zero/rest/internal/errcode"
@@ -173,28 +172,8 @@ func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, a
173172
}
174173
}
175174

176-
func doMarshalJson(v any) ([]byte, error) {
177-
// why not use json.Marshal? https://github.com/golang/go/issues/28453
178-
// it change the behavior of json.Marshal, like & -> \u0026, < -> \u003c, > -> \u003e
179-
// which is not what we want in logic response
180-
var buf bytes.Buffer
181-
enc := json.NewEncoder(&buf)
182-
enc.SetEscapeHTML(false)
183-
if err := enc.Encode(v); err != nil {
184-
return nil, err
185-
}
186-
187-
bs := buf.Bytes()
188-
// Remove trailing newline added by json.Encoder.Encode
189-
if len(bs) > 0 && bs[len(bs)-1] == '\n' {
190-
bs = bs[:len(bs)-1]
191-
}
192-
193-
return bs, nil
194-
}
195-
196175
func doWriteJson(w http.ResponseWriter, code int, v any) error {
197-
bs, err := doMarshalJson(v)
176+
bs, err := jsonx.Marshal(v)
198177
if err != nil {
199178
http.Error(w, err.Error(), http.StatusInternalServerError)
200179
return fmt.Errorf("marshal json failed, error: %w", err)

rest/httpx/responses_test.go

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -443,105 +443,3 @@ func TestWriteJsonCtxMarshalFailed(t *testing.T) {
443443
})
444444
assert.Equal(t, http.StatusInternalServerError, w.code)
445445
}
446-
447-
func Test_doMarshalJson(t *testing.T) {
448-
type args struct {
449-
v any
450-
}
451-
452-
tests := []struct {
453-
name string
454-
args args
455-
want []byte
456-
wantErr assert.ErrorAssertionFunc
457-
}{
458-
{
459-
name: "nil",
460-
args: args{nil},
461-
want: []byte("null"),
462-
wantErr: assert.NoError,
463-
},
464-
{
465-
name: "string",
466-
args: args{"hello"},
467-
want: []byte(`"hello"`),
468-
wantErr: assert.NoError,
469-
},
470-
{
471-
name: "int",
472-
args: args{42},
473-
want: []byte("42"),
474-
wantErr: assert.NoError,
475-
},
476-
{
477-
name: "bool",
478-
args: args{true},
479-
want: []byte("true"),
480-
wantErr: assert.NoError,
481-
},
482-
{
483-
name: "struct",
484-
args: args{
485-
struct {
486-
Name string `json:"name"`
487-
}{Name: "test"},
488-
},
489-
want: []byte(`{"name":"test"}`),
490-
wantErr: assert.NoError,
491-
},
492-
{
493-
name: "slice",
494-
args: args{[]int{1, 2, 3}},
495-
want: []byte("[1,2,3]"),
496-
wantErr: assert.NoError,
497-
},
498-
{
499-
name: "map",
500-
args: args{map[string]int{"a": 1, "b": 2}},
501-
want: []byte(`{"a":1,"b":2}`),
502-
wantErr: assert.NoError,
503-
},
504-
{
505-
name: "unmarshalable type",
506-
args: args{complex(1, 2)},
507-
want: nil,
508-
wantErr: assert.Error,
509-
},
510-
{
511-
name: "channel type",
512-
args: args{make(chan int)},
513-
want: nil,
514-
wantErr: assert.Error,
515-
},
516-
{
517-
name: "url with query params",
518-
args: args{"https://example.com/api?name=test&age=25"},
519-
want: []byte(`"https://example.com/api?name=test&age=25"`),
520-
wantErr: assert.NoError,
521-
},
522-
{
523-
name: "url with encoded query params",
524-
args: args{"https://example.com/api?data=hello%20world&special=%26%3D"},
525-
want: []byte(`"https://example.com/api?data=hello%20world&special=%26%3D"`),
526-
wantErr: assert.NoError,
527-
},
528-
{
529-
name: "url with multiple query params",
530-
args: args{"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"},
531-
want: []byte(`"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"`),
532-
wantErr: assert.NoError,
533-
},
534-
}
535-
536-
for _, tt := range tests {
537-
tt := tt
538-
t.Run(tt.name, func(t *testing.T) {
539-
got, err := doMarshalJson(tt.args.v)
540-
if !tt.wantErr(t, err, fmt.Sprintf("doMarshalJson(%v)", tt.args.v)) {
541-
return
542-
}
543-
544-
assert.Equalf(t, string(tt.want), string(got), "doMarshalJson(%v)", tt.args.v)
545-
})
546-
}
547-
}

0 commit comments

Comments
 (0)