Go JSON

Posted on Aug 17, 2020

Marshaling from and Unmarshaling between JSON and structs in Go, is done using the built-in encoding/json package and often relies heavily on struct tags.

Struct tags can be used to define how a field name maps to a name in our JSON output. This is especially useful because the encoding/json package is only able to see exported fields, so we are forced to capitalize the field names that we want to encode.

The json struct tag syntax is json:"name,option". To ignore a field completely, use "-", as seen in the Password field. As the name implies, omitempty leaves out the field in the JSON output when left empty.

Go can marshal strings, integers, floats and time fields, but will also encode slices, arrays, maps, and nested structs correctly.

When you need to distinguish between zero values or missing values, you use a pointer value instead, as seen in the DeletedAt field.

type User struct {
    Username string `json:"username"`
    Password string `json:"-"`
}

type Page struct {
    Title      string            `json:"title"`
    NrOfViews  int               `json:"nr_of_views"`
    Tags       []string          `json:"tags"`
    User       User              `json:"user"`
    CustomData map[string]string `json:"custom_data"`
    CreatedAt  time.Time         `json:"created_at"`
    DeletedAt  *time.Time        `json:"deleted_at,omitempty"`
}

Encoding this structure is fairly straightforward using the json.Marshal() method.

page := Page{
    Title:     "My page",
    NrOfViews: 12,
    Tags:      []string{"go", "json"},
    User: User{
        Username: "gumuz",
        Password: "secret123",
    },
    CustomData: map[string]string{"os": "osx", "browser": "firefox"},
    CreatedAt:  time.Now(),
}

data, err := json.Marshal(page)
if err != nil {
    panic(err)
}

The resulting JSON will look like this, but without the indentation.

{
  "title": "My page",
  "nr_of_views": 12,
  "tags": [
    "go",
    "json"
  ],
  "user": {
    "username": "gumuz"
  },
  "custom_data": {
    "browser": "firefox",
    "os": "osx"
  },
  "created_at": "2020-08-17T21:02:35.242834+02:00"
}

We can see that the Password field was left out, as is the DeletedAt field since it was marked as omitempty.

We can decode the data by initializing an empty struct and using json.Unmarshal() to populate it.

var anotherPage Page

err = json.Unmarshal(data, &anotherPage)
if err != nil {
	panic(err)
}

When we compare the two structs, we can see they’re nearly identical. The Password field was intentionally left out when we marshaled the data to JSON, and we lost some precision on the CreatedAt field.

fmt.Println(fmt.Sprintf("%+v\n", page))
fmt.Println(fmt.Sprintf("%+v\n", anotherPage))

// {Title:My page NrOfViews:12 Tags:[go json] User:{Username:gumuz Password:secret123} CustomData:map[browser:firefox os:osx] CreatedAt:2020-08-17T21:02:35.242834 +0200 CEST m=+0.000288531 DeletedAt:<nil>}
// {Title:My page NrOfViews:12 Tags:[go json] User:{Username:gumuz Password:} CustomData:map[browser:firefox os:osx] CreatedAt:2020-08-17T21:02:35.242834 +0200 CEST DeletedAt:<nil>}

comments powered by Disqus