chore: refactor jsonx.Marshal (#5045)

Signed-off-by: Kevin Wan <wanjunfeng@gmail.com>
This commit is contained in:
Kevin Wan
2025-08-01 22:05:13 +08:00
committed by GitHub
parent 610a7345dc
commit d150248c52
4 changed files with 123 additions and 127 deletions

View File

@@ -8,9 +8,25 @@ import (
"strings" "strings"
) )
// Marshal marshals v into json bytes. // Marshal marshals v into json bytes, without escaping HTML and removes the trailing newline.
func Marshal(v any) ([]byte, error) { func Marshal(v any) ([]byte, error) {
return json.Marshal(v) // why not use json.Marshal? https://github.com/golang/go/issues/28453
// it changes the behavior of json.Marshal, like & -> \u0026, < -> \u003c, > -> \u003e
// which is not what we want in API responses
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
bs := buf.Bytes()
// Remove trailing newline added by json.Encoder.Encode
if len(bs) > 0 && bs[len(bs)-1] == '\n' {
bs = bs[:len(bs)-1]
}
return bs, nil
} }
// MarshalToString marshals v into a string. // MarshalToString marshals v into a string.

View File

@@ -1,6 +1,7 @@
package jsonx package jsonx
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
@@ -101,3 +102,105 @@ func TestUnmarshalFromReaderError(t *testing.T) {
err := UnmarshalFromReader(strings.NewReader(s), &v) err := UnmarshalFromReader(strings.NewReader(s), &v)
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func Test_doMarshalJson(t *testing.T) {
type args struct {
v any
}
tests := []struct {
name string
args args
want []byte
wantErr assert.ErrorAssertionFunc
}{
{
name: "nil",
args: args{nil},
want: []byte("null"),
wantErr: assert.NoError,
},
{
name: "string",
args: args{"hello"},
want: []byte(`"hello"`),
wantErr: assert.NoError,
},
{
name: "int",
args: args{42},
want: []byte("42"),
wantErr: assert.NoError,
},
{
name: "bool",
args: args{true},
want: []byte("true"),
wantErr: assert.NoError,
},
{
name: "struct",
args: args{
struct {
Name string `json:"name"`
}{Name: "test"},
},
want: []byte(`{"name":"test"}`),
wantErr: assert.NoError,
},
{
name: "slice",
args: args{[]int{1, 2, 3}},
want: []byte("[1,2,3]"),
wantErr: assert.NoError,
},
{
name: "map",
args: args{map[string]int{"a": 1, "b": 2}},
want: []byte(`{"a":1,"b":2}`),
wantErr: assert.NoError,
},
{
name: "unmarshalable type",
args: args{complex(1, 2)},
want: nil,
wantErr: assert.Error,
},
{
name: "channel type",
args: args{make(chan int)},
want: nil,
wantErr: assert.Error,
},
{
name: "url with query params",
args: args{"https://example.com/api?name=test&age=25"},
want: []byte(`"https://example.com/api?name=test&age=25"`),
wantErr: assert.NoError,
},
{
name: "url with encoded query params",
args: args{"https://example.com/api?data=hello%20world&special=%26%3D"},
want: []byte(`"https://example.com/api?data=hello%20world&special=%26%3D"`),
wantErr: assert.NoError,
},
{
name: "url with multiple query params",
args: args{"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"},
want: []byte(`"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"`),
wantErr: assert.NoError,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := Marshal(tt.args.v)
if !tt.wantErr(t, err, fmt.Sprintf("Marshal(%v)", tt.args.v)) {
return
}
assert.Equalf(t, string(tt.want), string(got), "Marshal(%v)", tt.args.v)
})
}
}

View File

@@ -1,15 +1,14 @@
package httpx package httpx
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"sync" "sync"
"github.com/zeromicro/go-zero/core/jsonx"
"github.com/zeromicro/go-zero/core/logc" "github.com/zeromicro/go-zero/core/logc"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/internal/errcode" "github.com/zeromicro/go-zero/rest/internal/errcode"
@@ -173,28 +172,8 @@ func doHandleError(w http.ResponseWriter, err error, handler func(error) (int, a
} }
} }
func doMarshalJson(v any) ([]byte, error) {
// why not use json.Marshal? https://github.com/golang/go/issues/28453
// it change the behavior of json.Marshal, like & -> \u0026, < -> \u003c, > -> \u003e
// which is not what we want in logic response
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
return nil, err
}
bs := buf.Bytes()
// Remove trailing newline added by json.Encoder.Encode
if len(bs) > 0 && bs[len(bs)-1] == '\n' {
bs = bs[:len(bs)-1]
}
return bs, nil
}
func doWriteJson(w http.ResponseWriter, code int, v any) error { func doWriteJson(w http.ResponseWriter, code int, v any) error {
bs, err := doMarshalJson(v) bs, err := jsonx.Marshal(v)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return fmt.Errorf("marshal json failed, error: %w", err) return fmt.Errorf("marshal json failed, error: %w", err)

View File

@@ -443,105 +443,3 @@ func TestWriteJsonCtxMarshalFailed(t *testing.T) {
}) })
assert.Equal(t, http.StatusInternalServerError, w.code) assert.Equal(t, http.StatusInternalServerError, w.code)
} }
func Test_doMarshalJson(t *testing.T) {
type args struct {
v any
}
tests := []struct {
name string
args args
want []byte
wantErr assert.ErrorAssertionFunc
}{
{
name: "nil",
args: args{nil},
want: []byte("null"),
wantErr: assert.NoError,
},
{
name: "string",
args: args{"hello"},
want: []byte(`"hello"`),
wantErr: assert.NoError,
},
{
name: "int",
args: args{42},
want: []byte("42"),
wantErr: assert.NoError,
},
{
name: "bool",
args: args{true},
want: []byte("true"),
wantErr: assert.NoError,
},
{
name: "struct",
args: args{
struct {
Name string `json:"name"`
}{Name: "test"},
},
want: []byte(`{"name":"test"}`),
wantErr: assert.NoError,
},
{
name: "slice",
args: args{[]int{1, 2, 3}},
want: []byte("[1,2,3]"),
wantErr: assert.NoError,
},
{
name: "map",
args: args{map[string]int{"a": 1, "b": 2}},
want: []byte(`{"a":1,"b":2}`),
wantErr: assert.NoError,
},
{
name: "unmarshalable type",
args: args{complex(1, 2)},
want: nil,
wantErr: assert.Error,
},
{
name: "channel type",
args: args{make(chan int)},
want: nil,
wantErr: assert.Error,
},
{
name: "url with query params",
args: args{"https://example.com/api?name=test&age=25"},
want: []byte(`"https://example.com/api?name=test&age=25"`),
wantErr: assert.NoError,
},
{
name: "url with encoded query params",
args: args{"https://example.com/api?data=hello%20world&special=%26%3D"},
want: []byte(`"https://example.com/api?data=hello%20world&special=%26%3D"`),
wantErr: assert.NoError,
},
{
name: "url with multiple query params",
args: args{"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"},
want: []byte(`"http://localhost:8080/users?page=1&limit=10&sort=name&order=asc"`),
wantErr: assert.NoError,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := doMarshalJson(tt.args.v)
if !tt.wantErr(t, err, fmt.Sprintf("doMarshalJson(%v)", tt.args.v)) {
return
}
assert.Equalf(t, string(tt.want), string(got), "doMarshalJson(%v)", tt.args.v)
})
}
}