Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert array of errors to JSON in Go

I have an array of errors (type of error), but when I try to return the client in JSON format, it arrives empty.

It was created this way:

var (
    ErrEmptyName = errors.New("Nome não pode ser vázio")
    ErrInvalidType = errors.New("Tipo de pessoa inválido")
)

func (p Person) Validate() []error {

    var errors []error

    if govalidator.IsNull(p.Name) {
        errors = append(errors, ErrEmptyName)
    }

    if p.Type != "F" && p.Type != "J" {
        errors = append(errors, ErrInvalidType)
    }

    return errors
)

In my Controller:

err := person.Validate()
c.JSON(422, gin.H{"errors" : err})

My Output:

{"errors":"[{}]"}
like image 828
Wilson Tamarozzi Avatar asked Dec 31 '16 15:12

Wilson Tamarozzi


2 Answers

The error type is an interface with a single Error() method, but it is not special to the json package (the Error() method is not called on it).

However, error values may hold values of static types which may be nicely marshalable, or they may define their own marshaling logic by implementing json.Marshaler. Simply converting an error to string by calling its Error() method means we're not honoring the custom marshaling logic.

So I would suggest to create our own error slice type, on which we can implement our marshaling logic which should be:

  • check if the error value implements json.Marshaler, and if so, let it marshal itself
  • else as a fallback case call error.Error() to "obtain" a string which can easily be marshaled

This is how it could look like:

type JSONErrs []error

func (je JSONErrs) MarshalJSON() ([]byte, error) {
    res := make([]interface{}, len(je))
    for i, e := range je {
        if _, ok := e.(json.Marshaler); ok {
            res[i] = e // e knows how to marshal itself
        } else {
            res[i] = e.Error() // Fallback to the error string
        }
    }
    return json.Marshal(res)
}

And this is how you can use it:

err := person.Validate()
c.JSON(422, gin.H{"errors" : JSONErrs(err)})

Let's test our JSONErrs. We're also use a custom error type which implements custom marshaling logic:

type MyErr struct{ line int }

func (me MyErr) Error() string { return "Invalid input!" }

func (me MyErr) MarshalJSON() ([]byte, error) {
    return json.Marshal(
        struct {
            Type, Error string
            AtLine      int
        }{"MyErr", me.Error(), me.line})
}

And the test code:

errs := []error{
    errors.New("first"),
    errors.New("second"),
    MyErr{16},
}

data, err := json.Marshal(JSONErrs(errs))
fmt.Println(string(data), err)

Output (try it on the Go Playground):

["first","second",{"Type":"MyErr","Error":"Invalid input!","AtLine":16}] <nil>
like image 153
icza Avatar answered Sep 27 '22 23:09

icza


An error type is an interface which must implement an Error() method that returns an error message as a string. This is defined here: https://golang.org/pkg/builtin/#error. The reason why the error type is an interface, is to allow for error types that can be type casted to retrieve more detailed information.

Functions like fmt.Println and log.Println automatically resolves error types to display the message from Error(), the JSON library however, does not. The simplest way to get around this problem is by converting the error messages in []error to a []string and encoding that.

Here's some example code to do that with a for loop.

errs := person.Validate()
strErrors := make([]string, len(errs))

for i, err := range errs {
    strErrors[i] = err.Error()
}

c.JSON(422, gin.H{"errors" : strErrors})
like image 37
1lann Avatar answered Sep 27 '22 23:09

1lann