Wilson Tamarozzi
Wilson Tamarozzi

Reputation: 646

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":"[{}]"}

Upvotes: 9

Views: 7854

Answers (2)

icza
icza

Reputation: 418505

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>

Upvotes: 7

1lann
1lann

Reputation: 667

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})

Upvotes: 2

Related Questions