Skip to content

Commit 5e791b0

Browse files
hoshsadiqaldas
andauthored
Allow for custom JSON encoding implementations (#1880)
* Allow for custom JSON encoding implementations Co-authored-by: toimtoimtoim <[email protected]>
1 parent fd7a8a9 commit 5e791b0

File tree

5 files changed

+151
-18
lines changed

5 files changed

+151
-18
lines changed

bind.go

+6-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package echo
22

33
import (
44
"encoding"
5-
"encoding/json"
65
"encoding/xml"
76
"errors"
87
"fmt"
@@ -66,13 +65,13 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
6665
ctype := req.Header.Get(HeaderContentType)
6766
switch {
6867
case strings.HasPrefix(ctype, MIMEApplicationJSON):
69-
if err = json.NewDecoder(req.Body).Decode(i); err != nil {
70-
if ute, ok := err.(*json.UnmarshalTypeError); ok {
71-
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err)
72-
} else if se, ok := err.(*json.SyntaxError); ok {
73-
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
68+
if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
69+
switch err.(type) {
70+
case *HTTPError:
71+
return err
72+
default:
73+
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
7474
}
75-
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
7675
}
7776
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
7877
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {

context.go

+5-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package echo
22

33
import (
44
"bytes"
5-
"encoding/json"
65
"encoding/xml"
76
"fmt"
87
"io"
@@ -457,17 +456,16 @@ func (c *context) String(code int, s string) (err error) {
457456
}
458457

459458
func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error) {
460-
enc := json.NewEncoder(c.response)
461-
_, pretty := c.QueryParams()["pretty"]
462-
if c.echo.Debug || pretty {
463-
enc.SetIndent("", " ")
459+
indent := ""
460+
if _, pretty := c.QueryParams()["pretty"]; c.echo.Debug || pretty {
461+
indent = defaultIndent
464462
}
465463
c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)
466464
c.response.WriteHeader(code)
467465
if _, err = c.response.Write([]byte(callback + "(")); err != nil {
468466
return
469467
}
470-
if err = enc.Encode(i); err != nil {
468+
if err = c.echo.JSONSerializer.Serialize(c, i, indent); err != nil {
471469
return
472470
}
473471
if _, err = c.response.Write([]byte(");")); err != nil {
@@ -477,13 +475,9 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
477475
}
478476

479477
func (c *context) json(code int, i interface{}, indent string) error {
480-
enc := json.NewEncoder(c.response)
481-
if indent != "" {
482-
enc.SetIndent("", indent)
483-
}
484478
c.writeContentType(MIMEApplicationJSONCharsetUTF8)
485479
c.response.Status = code
486-
return enc.Encode(i)
480+
return c.echo.JSONSerializer.Serialize(c, i, indent)
487481
}
488482

489483
func (c *context) JSON(code int, i interface{}) (err error) {

echo.go

+8
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type (
9090
HidePort bool
9191
HTTPErrorHandler HTTPErrorHandler
9292
Binder Binder
93+
JSONSerializer JSONSerializer
9394
Validator Validator
9495
Renderer Renderer
9596
Logger Logger
@@ -125,6 +126,12 @@ type (
125126
Validate(i interface{}) error
126127
}
127128

129+
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
130+
JSONSerializer interface {
131+
Serialize(c Context, i interface{}, indent string) error
132+
Deserialize(c Context, i interface{}) error
133+
}
134+
128135
// Renderer is the interface that wraps the Render function.
129136
Renderer interface {
130137
Render(io.Writer, string, interface{}, Context) error
@@ -315,6 +322,7 @@ func New() (e *Echo) {
315322
e.TLSServer.Handler = e
316323
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
317324
e.Binder = &DefaultBinder{}
325+
e.JSONSerializer = &DefaultJSONSerializer{}
318326
e.Logger.SetLevel(log.ERROR)
319327
e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
320328
e.pool.New = func() interface{} {

json.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package echo
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
// DefaultJSONSerializer implements JSON encoding using encoding/json.
10+
type DefaultJSONSerializer struct{}
11+
12+
// Serialize converts an interface into a json and writes it to the response.
13+
// You can optionally use the indent parameter to produce pretty JSONs.
14+
func (d DefaultJSONSerializer) Serialize(c Context, i interface{}, indent string) error {
15+
enc := json.NewEncoder(c.Response())
16+
if indent != "" {
17+
enc.SetIndent("", indent)
18+
}
19+
return enc.Encode(i)
20+
}
21+
22+
// Deserialize reads a JSON from a request body and converts it into an interface.
23+
func (d DefaultJSONSerializer) Deserialize(c Context, i interface{}) error {
24+
err := json.NewDecoder(c.Request().Body).Decode(i)
25+
if ute, ok := err.(*json.UnmarshalTypeError); ok {
26+
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err)
27+
} else if se, ok := err.(*json.SyntaxError); ok {
28+
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
29+
}
30+
return err
31+
}

json_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package echo
2+
3+
import (
4+
testify "github.com/stretchr/testify/assert"
5+
"net/http"
6+
"net/http/httptest"
7+
"strings"
8+
"testing"
9+
)
10+
11+
// Note this test is deliberately simple as there's not a lot to test.
12+
// Just need to ensure it writes JSONs. The heavy work is done by the context methods.
13+
func TestDefaultJSONCodec_Encode(t *testing.T) {
14+
e := New()
15+
req := httptest.NewRequest(http.MethodPost, "/", nil)
16+
rec := httptest.NewRecorder()
17+
c := e.NewContext(req, rec).(*context)
18+
19+
assert := testify.New(t)
20+
21+
// Echo
22+
assert.Equal(e, c.Echo())
23+
24+
// Request
25+
assert.NotNil(c.Request())
26+
27+
// Response
28+
assert.NotNil(c.Response())
29+
30+
//--------
31+
// Default JSON encoder
32+
//--------
33+
34+
enc := new(DefaultJSONSerializer)
35+
36+
err := enc.Serialize(c, user{1, "Jon Snow"}, "")
37+
if assert.NoError(err) {
38+
assert.Equal(userJSON+"\n", rec.Body.String())
39+
}
40+
41+
req = httptest.NewRequest(http.MethodPost, "/", nil)
42+
rec = httptest.NewRecorder()
43+
c = e.NewContext(req, rec).(*context)
44+
err = enc.Serialize(c, user{1, "Jon Snow"}, " ")
45+
if assert.NoError(err) {
46+
assert.Equal(userJSONPretty+"\n", rec.Body.String())
47+
}
48+
}
49+
50+
// Note this test is deliberately simple as there's not a lot to test.
51+
// Just need to ensure it writes JSONs. The heavy work is done by the context methods.
52+
func TestDefaultJSONCodec_Decode(t *testing.T) {
53+
e := New()
54+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
55+
rec := httptest.NewRecorder()
56+
c := e.NewContext(req, rec).(*context)
57+
58+
assert := testify.New(t)
59+
60+
// Echo
61+
assert.Equal(e, c.Echo())
62+
63+
// Request
64+
assert.NotNil(c.Request())
65+
66+
// Response
67+
assert.NotNil(c.Response())
68+
69+
//--------
70+
// Default JSON encoder
71+
//--------
72+
73+
enc := new(DefaultJSONSerializer)
74+
75+
var u = user{}
76+
err := enc.Deserialize(c, &u)
77+
if assert.NoError(err) {
78+
assert.Equal(u, user{ID: 1, Name: "Jon Snow"})
79+
}
80+
81+
var userUnmarshalSyntaxError = user{}
82+
req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(invalidContent))
83+
rec = httptest.NewRecorder()
84+
c = e.NewContext(req, rec).(*context)
85+
err = enc.Deserialize(c, &userUnmarshalSyntaxError)
86+
assert.IsType(&HTTPError{}, err)
87+
assert.EqualError(err, "code=400, message=Syntax error: offset=1, error=invalid character 'i' looking for beginning of value, internal=invalid character 'i' looking for beginning of value")
88+
89+
var userUnmarshalTypeError = struct {
90+
ID string `json:"id"`
91+
Name string `json:"name"`
92+
}{}
93+
94+
req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
95+
rec = httptest.NewRecorder()
96+
c = e.NewContext(req, rec).(*context)
97+
err = enc.Deserialize(c, &userUnmarshalTypeError)
98+
assert.IsType(&HTTPError{}, err)
99+
assert.EqualError(err, "code=400, message=Unmarshal type error: expected=string, got=number, field=id, offset=7, internal=json: cannot unmarshal number into Go struct field .id of type string")
100+
101+
}

0 commit comments

Comments
 (0)