JSON Processing in Golang Explained

golang JSON processing tutorial

To Unmarshal() or To Decode() for JSON?

If you are like me and want to learn backend development, you probably came across JSON handling at one point. JSON is a very popular format for transferring data between the front and backend. Because it is such an essential feature in modern web development, Golang adds support for JSON in its encoding/json package.

The problem is that there isn’t only one way of doing things. If you have watched a couple of tutorials in the past, you may have noticed people using different functions to handle JSON. Some people use Marshal and Unmarshal, while others use Encode and Decode. What should you use? Which one is better? In this blog post, I will explain the difference between the two approaches and when you should use one over the other. Enjoy!

Two approaches for JSON processing with Golang

There are two ways to read and write JSON. This code snippet will help you understand how to use the two approaches. First, Marshal and Unmarshal:

func PrettyPrint(v interface{}) (err error) {
b, err := json.MarshalIndent(v, "", "\t")
if err == nil {
fmt.Println(string(b))
}
return err
}func TryMarshal() error {
data := map[string]interface{}{
"1": "one",
"2": "two",
"3": "three",
}
result, err := json.Marshal(&data)
if err != nil {
return err
} err = PrettyPrint(result)
if err != nil {
return err
} return nil
}func TryUnmarshal() error {
myFile, err := os.Open("test.csv")
if err != nil {
return err
}
defer myFile.Close() data, err := io.ReadAll(myFile)
if err != nil {
return err
} var result map[string]interface{}
json.Unmarshal([]byte(data), &result) err = PrettyPrint(result)
if err != nil {
return err
} return nil
}

In TryMarshal, I created a map[string]interface{} to hold some data. I then passed it to Marshal.

In TryUnmarshal, I read a file and converted it into a byte slice data. That data is passed into Unmarshal, which stores the result in a map[string]interface{}.

PrettyPrint just formats the output to make it look nice.

Now let’s take a look at Encoder.Encode and Decoder.Decode.

func TryEncode() error {
data := map[string]interface{}{
"1": "one",
"2": "two",
"3": "three",
}
err := json.NewEncoder(os.Stdout).Encode(&data)
if err != nil {
return err
} return nil
}func TryDecode(path string) error {
myFile, err := os.Open(path)
if err != nil {
return err
}
defer myFile.Close() var result map[string]interface{}
json.NewDecoder(myFile).Decode(&result) return nil
}

The code looks pretty similar to the previous example. TryEncode is analogous to TryMarhsal and TryDecode is analogous to TryUnmarshal. The only difference here is that Encode and Decode are methods to Encoder and Decoder types.

NewEncoder takes in the io.Writer interface and returns a new Encoder type. NewDecoder takes in the io.Reader interface and returns a new Decoder type. For this example, I passed in os.Stdout for NewEncoder and myFile (which is of os.File type) for NewDecoder.

Now that we know how to use these functions, we can dive into how the two approaches differ under the hood.

Golang Marshal() and Unmarshal()

Let’s take a look at their implementation: