Skip to content

Commit

Permalink
Allow for custom JSON encoding implementations (#1880)
Browse files Browse the repository at this point in the history
* Allow for custom JSON encoding implementations

Co-authored-by: toimtoimtoim <[email protected]>
  • Loading branch information
hoshsadiq and aldas authored Jul 5, 2021
1 parent fd7a8a9 commit 5e791b0
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 18 deletions.
13 changes: 6 additions & 7 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package echo

import (
"encoding"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
Expand Down Expand Up @@ -66,13 +65,13 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
ctype := req.Header.Get(HeaderContentType)
switch {
case strings.HasPrefix(ctype, MIMEApplicationJSON):
if err = json.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*json.UnmarshalTypeError); ok {
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)
} else if se, ok := err.(*json.SyntaxError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
if err = c.Echo().JSONSerializer.Deserialize(c, i); err != nil {
switch err.(type) {
case *HTTPError:
return err
default:
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
}
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
Expand Down
16 changes: 5 additions & 11 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package echo

import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
Expand Down Expand Up @@ -457,17 +456,16 @@ func (c *context) String(code int, s string) (err error) {
}

func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error) {
enc := json.NewEncoder(c.response)
_, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
enc.SetIndent("", " ")
indent := ""
if _, pretty := c.QueryParams()["pretty"]; c.echo.Debug || pretty {
indent = defaultIndent
}
c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)
c.response.WriteHeader(code)
if _, err = c.response.Write([]byte(callback + "(")); err != nil {
return
}
if err = enc.Encode(i); err != nil {
if err = c.echo.JSONSerializer.Serialize(c, i, indent); err != nil {
return
}
if _, err = c.response.Write([]byte(");")); err != nil {
Expand All @@ -477,13 +475,9 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
}

func (c *context) json(code int, i interface{}, indent string) error {
enc := json.NewEncoder(c.response)
if indent != "" {
enc.SetIndent("", indent)
}
c.writeContentType(MIMEApplicationJSONCharsetUTF8)
c.response.Status = code
return enc.Encode(i)
return c.echo.JSONSerializer.Serialize(c, i, indent)
}

func (c *context) JSON(code int, i interface{}) (err error) {
Expand Down
8 changes: 8 additions & 0 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type (
HidePort bool
HTTPErrorHandler HTTPErrorHandler
Binder Binder
JSONSerializer JSONSerializer
Validator Validator
Renderer Renderer
Logger Logger
Expand Down Expand Up @@ -125,6 +126,12 @@ type (
Validate(i interface{}) error
}

// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
JSONSerializer interface {
Serialize(c Context, i interface{}, indent string) error
Deserialize(c Context, i interface{}) error
}

// Renderer is the interface that wraps the Render function.
Renderer interface {
Render(io.Writer, string, interface{}, Context) error
Expand Down Expand Up @@ -315,6 +322,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{}
e.JSONSerializer = &DefaultJSONSerializer{}
e.Logger.SetLevel(log.ERROR)
e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} {
Expand Down
31 changes: 31 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package echo

import (
"encoding/json"
"fmt"
"net/http"
)

// DefaultJSONSerializer implements JSON encoding using encoding/json.
type DefaultJSONSerializer struct{}

// Serialize converts an interface into a json and writes it to the response.
// You can optionally use the indent parameter to produce pretty JSONs.
func (d DefaultJSONSerializer) Serialize(c Context, i interface{}, indent string) error {
enc := json.NewEncoder(c.Response())
if indent != "" {
enc.SetIndent("", indent)
}
return enc.Encode(i)
}

// Deserialize reads a JSON from a request body and converts it into an interface.
func (d DefaultJSONSerializer) Deserialize(c Context, i interface{}) error {
err := json.NewDecoder(c.Request().Body).Decode(i)
if ute, ok := err.(*json.UnmarshalTypeError); ok {
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)
} else if se, ok := err.(*json.SyntaxError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
}
return err
}
101 changes: 101 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package echo

import (
testify "github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// Note this test is deliberately simple as there's not a lot to test.
// Just need to ensure it writes JSONs. The heavy work is done by the context methods.
func TestDefaultJSONCodec_Encode(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec).(*context)

assert := testify.New(t)

// Echo
assert.Equal(e, c.Echo())

// Request
assert.NotNil(c.Request())

// Response
assert.NotNil(c.Response())

//--------
// Default JSON encoder
//--------

enc := new(DefaultJSONSerializer)

err := enc.Serialize(c, user{1, "Jon Snow"}, "")
if assert.NoError(err) {
assert.Equal(userJSON+"\n", rec.Body.String())
}

req = httptest.NewRequest(http.MethodPost, "/", nil)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec).(*context)
err = enc.Serialize(c, user{1, "Jon Snow"}, " ")
if assert.NoError(err) {
assert.Equal(userJSONPretty+"\n", rec.Body.String())
}
}

// Note this test is deliberately simple as there's not a lot to test.
// Just need to ensure it writes JSONs. The heavy work is done by the context methods.
func TestDefaultJSONCodec_Decode(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec).(*context)

assert := testify.New(t)

// Echo
assert.Equal(e, c.Echo())

// Request
assert.NotNil(c.Request())

// Response
assert.NotNil(c.Response())

//--------
// Default JSON encoder
//--------

enc := new(DefaultJSONSerializer)

var u = user{}
err := enc.Deserialize(c, &u)
if assert.NoError(err) {
assert.Equal(u, user{ID: 1, Name: "Jon Snow"})
}

var userUnmarshalSyntaxError = user{}
req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(invalidContent))
rec = httptest.NewRecorder()
c = e.NewContext(req, rec).(*context)
err = enc.Deserialize(c, &userUnmarshalSyntaxError)
assert.IsType(&HTTPError{}, err)
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")

var userUnmarshalTypeError = struct {
ID string `json:"id"`
Name string `json:"name"`
}{}

req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
rec = httptest.NewRecorder()
c = e.NewContext(req, rec).(*context)
err = enc.Deserialize(c, &userUnmarshalTypeError)
assert.IsType(&HTTPError{}, err)
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")

}

0 comments on commit 5e791b0

Please sign in to comment.