Lewislbr

Using JSON in Go

Go can serialize JSON from/to structs or maps.

Structs can have tags attached to the fields that provide metadata to use when converting them. These tags can specify the name of the JSON key, to omit fields if they are empty, and to not include fields.

For example this struct:

var item = struct {
	ID         string    `json:"id"`
	Name       string    `json:"name,omitempty"`
	CreatedAt  time.Time `json:"created_at"`
	SecretCode string    `json:"-"`
}{
	ID:         "foo",
	Name:       "",
	CreatedAt:  time.Now(),
	SecretCode: "qux",
}

Would result in the following JSON:

{
  "id": "foo",
  "created_at": "2009-11-10T23:00:00Z"
}

JSON conversion

Converting a struct into JSON:

func main() {
	data := struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}{
		ID:   "qux",
		Name: "foo",
	}
	payload, err := json.Marshal(data)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(payload))
}

// Output:
// {"id":"qux","name":"foo"}

To prettify the JSON output it can be used:

payload, err := json.MarshalIndent(item, "", "  ")

Converting a map into JSON:

func main() {
	data := map[string]string{"id": "qux", "name": "foo"}
	payload, err := json.Marshal(data)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(payload))
}

// Output:
// {"id":"qux","name":"foo"}

Converting JSON into a struct:

func main() {
	payload := `{"id":"qux","name":"foo"}`
	data := struct {
		ID   string
		Name string
	}{}
	err := json.Unmarshal([]byte(payload), &data)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("%+v\n", data)
}

// Output:
// {ID:qux Name:foo}

Converting JSON into a map:

func main() {
	payload := `{"id":"qux","name":"foo"}`
	data := map[string]string{}
	err := json.Unmarshal([]byte(payload), &data)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(data)
}

// Output:
// map[id:qux name:foo]

JSON payloads

When working with streams (for example in HTTP responses), json.Encoder can be used for better performance, as it will directly write the JSON result into the response. However, if an error happens during the process, a partial response will have been sent.

func handler(w http.ResponseWriter, r *http.Request) {
	data := struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}{
		ID:   "qux",
		Name: "foo",
	}

	w.Header().Set("content-type", "application/json")

	err := json.NewEncoder(w).Encode(data)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		log.Println(err)
		return
	}
}

To make sure the entire response is correct before sending it, it can be marshaled first and then sent.

func handler(w http.ResponseWriter, r *http.Request) {
	data := struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}{
		ID:   "qux",
		Name: "foo",
	}
	payload, err := json.Marshal(data)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		log.Println(err)
		return
	}

	w.Header().Set("content-type", "application/json")
	w.Write(payload)
}

A gotcha to take into account when sending empty slices: if a slice is declared with var, like var data []string, and is sent empty, it will be sent as null instead of []. This is due to the fact that it has been initialized as a nil pointer, so it gets encoded as null. To avoid it, we should initialize it as an empty slice like data := []string{}, as mentioned in Go Code Review Comments.



If you're using dark mode, do you like the code blocks's theme? I have it available for VS Code, feel free to check it.