Handling JSON in Go

· 1194 words · 6 minute read

Go has a gigantic standard library and of course JSON encoding as well as decoding is included.

Converting a go struct to a JSON object ##

We first start of by defining a Go structure as a type and afterwards creating a new variable with an instance of this type…

 1// main.go
 2package main
 3
 4type Car struct {
 5	Name  string `json:"name"`
 6	Age   int    `json:"age"`
 7	Model string `json:"model"`
 8}
 9
10type Person struct {
11	Name     string `json:"name"`
12	Age      int    `json:"age"`
13	Hometown string `json:"home_town"`
14	Cars     []Car  `json:"cars"`
15}

I use a fairly complex data structure here to showcase converting nested structs to json.

Tip

Notice the raw go strings behind each property?

The text in between the quotation marks tells the json parser / generator which Go structure key has what json object key.

1type TestStruct struct {
2    TestName string `json:"name"`
3}
4test := TestStruct{ TestName: "text" }

Converting the above to json, yields the result below:

1{
2  "name": "text"
3}

To convert structures to json objects we will need the encoding/json package imported from the standard library.:

 1// main.go
 2package main
 3
 4import (
 5	"encoding/json"
 6)
 7
 8type Car struct {
 9	Name  string `json:"name"`
10	Age   int    `json:"age"`
11	Model string `json:"model"`
12}
13
14type Person struct {
15	Name     string `json:"name"`
16	Age      int    `json:"age"`
17	Hometown string `json:"home_town"`
18	Cars     []Car  `json:"cars"`
19}

Now we create a new variable of type Person and assign some data to it:

 1// main.go
 2package main
 3
 4import (
 5	"encoding/json"
 6)
 7
 8type Car struct {
 9	Name  string `json:"name"`
10	Age   int    `json:"age"`
11	Model string `json:"model"`
12}
13
14type Person struct {
15	Name     string `json:"name"`
16	Age      int    `json:"age"`
17	Hometown string `json:"home_town"`
18	Cars     []Car  `json:"cars"`
19}
20
21func main(){
22    data := Person{
23		Name:     "John",
24		Age:      30,
25		Hometown: "New York",
26		Cars: []Car{
27			{Name: "Ford", Age: 2014, Model: "F150"},
28			{Name: "Mercedes", Age: 2003, Model: "R class"},
29		},
30	}
31}

To convert this Person structure to json we call the json.Marshall function which returns the converted json as a bytes array and print it as a string:

 1// main.go
 2package main
 3
 4import (
 5	"encoding/json"
 6)
 7
 8type Car struct {
 9	Name  string `json:"name"`
10	Age   int    `json:"age"`
11	Model string `json:"model"`
12}
13
14type Person struct {
15	Name     string `json:"name"`
16	Age      int    `json:"age"`
17	Hometown string `json:"home_town"`
18	Cars     []Car  `json:"cars"`
19}
20
21func main(){
22    data := Person{
23		Name:     "John",
24		Age:      30,
25		Hometown: "New York",
26		Cars: []Car{
27			{Name: "Ford", Age: 2014, Model: "F150"},
28			{Name: "Mercedes", Age: 2003, Model: "R class"},
29		},
30	}
31
32    res, err := json.Marshal(data)
33
34	if err != nil {
35		log.Fatalln("failed to convert struct to json", err)
36	}
37
38	log.Println(string(res))
39}

Of course we need to handle the error json.Marshal could return if converting the structure to a json object failed.

Running the above results in the following output:

1$ go run .
22023/01/27 13:02:45 {"name":"John","age":30,"home_town":"New York","cars":[{"name":"Ford","age":2014,"model":"F150"},{"name":"Mercedes","age":2003,"model":"R class"}]}

The pretty printed json looks like this:

1{
2  "name": "John",
3  "age": 30,
4  "home_town": "New York",
5  "cars": [
6    { "name": "Ford", "age": 2014, "model": "F150" },
7    { "name": "Mercedes", "age": 2003, "model": "R class" }
8  ]
9}

Converting a JSON object to a go struct ##

To reverse the previously generated json back to a struct we use the json.Unmarshal method…

 1package main
 2
 3import (
 4	"encoding/json"
 5	"log"
 6)
 7
 8type Car struct {
 9	Name  string `json:"name"`
10	Age   int    `json:"age"`
11	Model string `json:"model"`
12}
13
14type Person struct {
15	Name     string `json:"name"`
16	Age      int    `json:"age"`
17	Hometown string `json:"home_town"`
18	Cars     []Car  `json:"cars"`
19}
20
21func main() {
22	data := Person{
23		Name:     "John",
24		Age:      30,
25		Hometown: "New York",
26		Cars: []Car{
27			{Name: "Ford", Age: 2014, Model: "F150"},
28			{Name: "Mercedes", Age: 2003, Model: "R class"},
29		},
30	}
31
32	res, err := json.Marshal(data)
33
34	if err != nil {
35		log.Fatalln("failed to convert struct to json", err)
36	}
37
38	log.Println(string(res))
39
40	newPerson := Person{}
41	err = json.Unmarshal(res, &newPerson)
42
43	if err != nil {
44		log.Fatalln("failed to convert json back to struct", err)
45	}
46
47	log.Println(newPerson)
48}

To use the unmarshal function we have to pass it a byte array (res []byte) as the first parameter and provide a reference to a variable we want the json to be converted into as the second parameter (&newPerson &Person). json.Unmarshal can return an error if converting the json to a struct fails. We catch this error just like before.

At the end we print the result. The whole program now prints the following:

1$ go run .
22023/01/27 13:14:36 {"name":"John","age":30,"home_town":"New York","cars":[{"name":"Ford","age":2014,"model":"F150"},{"name":"Mercedes","age":2003,"model":"R class"}]}
32023/01/27 13:14:36 {John 30 New York [{Ford 2014 F150} {Mercedes 2003 R class}]}

HTTP server example ##

The following is a simple http server setup in ~50 lines, which returns the requester a go structure as a JSON string.

 1package main
 2
 3import (
 4	"encoding/json"
 5	"fmt"
 6	"io"
 7	"log"
 8	"net/http"
 9	"time"
10)
11
12const PORT = "8080"
13
14// define the data we want to convert to json using the raw strings after the struct fields
15type RequestData struct {
16	Method      string `json:"method"`
17	URL         string `json:"url"`
18	RemoteAddr  string `json:"remote_addr"`
19	StatusCode  int    `json:"status_code"`
20	Status      string `json:"status"`
21	RequestTime string `json:"request_time"`
22}
23
24func getRoot(w http.ResponseWriter, r *http.Request) {
25    // timestamp used for calculating the duration the response took the server to complete
26	begin := time.Now()
27
28	log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
29
30    // define variable of type RequestData and fill its fields with data
31	data := RequestData{
32		Method:      r.Method,
33		URL:         r.URL.Path,
34		RemoteAddr:  r.RemoteAddr,
35		StatusCode:  http.StatusOK,
36		Status:      fmt.Sprint(http.StatusOK) + " " + http.StatusText(http.StatusOK),
37		RequestTime: fmt.Sprint(time.Since(begin).Milliseconds()) + "ms",
38	}
39
40    // convert the above variable to its json representation
41	res, err := json.Marshal(data)
42
43    // handle possible errors by returning 500 as a statuscode
44	if err != nil {
45		w.WriteHeader(http.StatusInternalServerError)
46		io.WriteString(w, http.StatusText(http.StatusInternalServerError))
47		return
48	}
49
50	w.Header().Set("Content-Type", "application/json")
51	io.WriteString(w, string(res))
52}
53
54func main() {
55	http.HandleFunc("/", getRoot)
56	log.Println("Listening on port", PORT)
57	log.Fatalln(http.ListenAndServe(":"+PORT, nil))
58}

Calling the above yields the following output:

1$ go run .
22023/01/27 13:36:56 Listening on port 8080
32023/01/27 13:37:01 [GET] / 127.0.0.1:51832

Curling the endpoints returns the following json:

 1$ curl -v localhost:8080
 2*   Trying 127.0.0.1:8080...
 3* Connected to localhost (127.0.0.1) port 8080 (#0)
 4> GET / HTTP/1.1
 5> Host: localhost:8080
 6> User-Agent: curl/7.87.0
 7> Accept: */*
 8>
 9* Mark bundle as not supporting multiuse
10< HTTP/1.1 200 OK
11< Content-Type: application/json
12< Date: Fri, 27 Jan 2023 12:35:33 GMT
13< Content-Length: 115
14<
15* Connection #0 to host localhost left intact
16{"method":"GET","url":"/","remote_addr":"127.0.0.1:34920","status_code":200,"status":"200 OK","request_time":"0ms"}

The pretty printed json:

1{
2  "method": "GET",
3  "url": "/",
4  "remote_addr": "127.0.0.1:34920",
5  "status_code": 200,
6  "status": "200 OK",
7  "request_time": "0ms"
8}