Skip to content

Allow for custom JSON encoding implementations #1880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")

}