Custom Error Marshaling to JSON in Go

Reading Time: 2 minutes

In this post I want to go through an example of data marshaling in the Go language. The example starts with this snippet:

package main

import (
  "encoding/json"
  "errors"
  "fmt"
)

type AModel struct {
  Name   string
  Errors []error
}

func main() {
  amodel := AModel{"Mr Model",
    []error{errors.New("e1"), errors.New("e2")}}

  data, marshalError := json.Marshal(amodel)

  if marshalError != nil {
    panic(data)
  }
  fmt.Println(string(data))
}

I have a struct with two fields: the first field is a Name string and the second is an Errors array of error. I want to marshal that struct to JSON, and I want an output like the following:

{"Name":"Mr Model","Errors":["e1", "e2"]}

If you run the snippet the output you get is:

{"Name":"Mr Model","Errors":[{},{}]}

I'm not getting the expected messages! The problem here is that I assumed that json.Marshal was going to know how to marshal an error interface, which didn't happen. Now, If we look at the json.Marshal documentation, something stands out:

If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON.

But how do I give my error elements a MarshalJSON method?.

Answer 1: embed the error in your own error structure and add the proper member function.

type MyError struct {
    error
}

func (me MyError) MarshalJSON() ([]byte, error) {
    return json.Marshal(me.Error())
}

And then create the errors this way:

amodel := AModel{"Mr Model",
    []error{MyError{errors.New("e1")}, MyError{errors.New("e2")}}
}

Running the example with those changes would now output:

{"Name":"Mr Model","Errors":["e1","e2"]}

Having to create a new MyError that is just wrapping the original errors now seems unnecessary, so I went to the Gopher Academy Slack community and asked about it, and Joe Shaw gave me an interesting answer:

Answer 2: Implement a type based on []error and implement MarshalJSON instead.

First implement our own type:

type MarshalableErrors []error

And then implement a more involved MarshalJSON example:

func (me MarshalableErrors) MarshalJSON() ([]byte, error) {
  data := []byte("[")
  for i, err := range me {
    if i != 0 {
      data = append(data, ',')
    }

    j, err := json.Marshal(err.Error())
    if err != nil {
      return nil, err
    }

    data = append(data, j...)
  }
  data = append(data, ']')

  return data, nil
}

And then use it as in:

type AModel struct {
  Name   string
  Errors MarshalableErrors
}

func main() {
  amodel := AModel{"Mr Model",
    []error{errors.New("e1"), errors.New("e2")}}

  data, marshalError := json.Marshal(amodel)

  if marshalError != nil {
    panic(marshalError)
  }
  fmt.Println(string(data))
}

Try it here.

For this example I prefer to go with the second answer because it allows me keep my errors creation simple by delegating the JSON marshalling process to single function.

Remember to follow us on Twitter for more updates on GoLang, and other interesting tech posts.

Thank you for reading.

0 Shares:
You May Also Like