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 1 commit
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
Prev Previous commit
Next Next commit
Allow both JSON encode and decode to be overwritten
  • Loading branch information
hoshsadiq committed Jun 13, 2021
commit ebe6dbe974836b4a4b7953203cdbcd66bf543704
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().JSONCodec.Decode(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
4 changes: 2 additions & 2 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
if _, err = c.response.Write([]byte(callback + "(")); err != nil {
return
}
if err = c.echo.JSONEncoder.JSON(i, indent, c); err != nil {
if err = c.echo.JSONCodec.Encode(c, i, indent); err != nil {
return
}
if _, err = c.response.Write([]byte(");")); err != nil {
Expand All @@ -477,7 +477,7 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
func (c *context) json(code int, i interface{}, indent string) error {
c.writeContentType(MIMEApplicationJSONCharsetUTF8)
c.response.Status = code
return c.echo.JSONEncoder.JSON(i, indent, c)
return c.echo.JSONCodec.Encode(c, i, indent)
}

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

// JSONEncoder is the interface that encodes an interface{} into a JSON string.
JSONEncoder interface {
JSON(i interface{}, indent string, c Context) error
// JSONCodec is the interface that encodes and decodes JSON to and from interfaces.
JSONCodec interface {
Encode(c Context, i interface{}, indent string) error
Decode(c Context, i interface{}) error
}

// Renderer is the interface that wraps the Render function.
Expand Down Expand Up @@ -321,7 +322,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{}
e.JSONEncoder = &DefaultJSONEncoder{}
e.JSONCodec = &DefaultJSONCodec{}
e.Logger.SetLevel(log.ERROR)
e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} {
Expand Down
26 changes: 21 additions & 5 deletions json.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
package echo

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

// DefaultJSONEncoder implements JSON encoding using encoding/json.
type DefaultJSONEncoder struct{}
// DefaultJSONCodec implements JSON encoding using encoding/json.
type DefaultJSONCodec struct{}

// JSON converts an interface into a json and writes it to the response.
func (d DefaultJSONEncoder) JSON(i interface{}, indent string, c Context) error {
// Encode 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 DefaultJSONCodec) Encode(c Context, i interface{}, indent string) error {
enc := json.NewEncoder(c.Response())
if indent != "" {
enc.SetIndent("", indent)
}
return enc.Encode(i)
}

// Decode reads a JSON from a request body and converts it into an interface.
func (d DefaultJSONCodec) Decode(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
}
62 changes: 58 additions & 4 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ 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 TestDefaultJSONEncoder_JSON(t *testing.T) {
func TestDefaultJSONCodec_Encode(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()
Expand All @@ -30,18 +31,71 @@ func TestDefaultJSONEncoder_JSON(t *testing.T) {
// Default JSON encoder
//--------

enc := new(DefaultJSONEncoder)
enc := new(DefaultJSONCodec)

err := enc.JSON(user{1, "Jon Snow"}, "", c)
err := enc.Encode(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.JSON(user{1, "Jon Snow"}, " ", c)
err = enc.Encode(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(DefaultJSONCodec)

var u = user{}
err := enc.Decode(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.Decode(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.Decode(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")

}