Go: Marshal and Unmarshal JSON with time and URL data

Go has a robust JSON library for consuming and outputting JSON. The standard json.Marshal() and json.Unmarshal() functions do a great job of aligning primitive types and converting them from Go into JSON, and vise versa. Beyond the basic types you will have to create custom marshaling methods on your types and write some logic to handle the conversion process. We will go over writing these custom methods for types that contain time and URL data.

The example we will work with is a Link data type, a struct that contains information about a web URL. It contains time.Time and url.URL data types, and a string and int for the title and ID.

1 2 3 4 5 6 type Link struct { ID int ` json:"id,string,omitempty" ` Title string ` json:"title,string" ` URL url . URL ` json:"url,string" ` CreatedAt time . Time ` json:"createdat,string" ` }

This gives us four unique types to work use, with url.URL and time.Time being the interesting ones.

JSON itself does not have complex data types for for time or URLs, they are stored as strings. In order to use them as data in Go we need be specific in how we get the strings and transform them into data.

We will use another building block of Go, the interface, to enhance our Link struct and allow for custom JSON handeling. The json package defines two interfaces, Marshaler and Unmarshaler. Let us look at Unmarshaler first.

Unmarshaling

In order for our Link type to implement the Unmarshaler interface we need to satisfy the interface method; which is UnmarshalJSON() . This method will be called when we use the json.Unmarshal() method instead of the standard unmarshal methods in the json package.

1 2 3 type Unmarshaler interface { UnmarshalJSON ( [ ] byte ) error }

The UnmarshalJSON() method takes a slice of bytes, processes it, stores the data in the type, and returns and error type. We will return nil if there is no error.

In our example here we are marshaling everything into a map of strings, with strings as the keys. Then we are iterating over the keys and manually Unmarshal the data into our struct depending on the type.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func ( l * Link ) UnmarshalJSON ( j [ ] byte ) error { var rawStrings map [ string ] string err := json . Unmarshal ( j , & rawStrings ) if err != nil { return err } for k , v := range rawStrings { if strings . ToLower ( k ) == "id" { l . ID , err = strconv . Atoi ( v ) if err != nil { return err } } if strings . ToLower ( k ) == "title" { l . Title = v } if strings . ToLower ( k ) == "url" { u , err := url . Parse ( v ) if err != nil { return err } l . URL = * u } if strings . ToLower ( k ) == "createdat" { t , err := time . Parse ( time . RFC3339 , v ) if err != nil { return err } l . CreatedAt = t } } return nil }

Parsing Time is done in the same way, except we need to match the time format in the parse function with the JSON data supplied.

The time value in the JSON could be in a variety of formats. In this example we are expecting that to be RFC3339. You will need to know the format of the time as it is used in the source JSON, and use Go’s time format to specify the layout so it can be correctly mapped to a time.Time object.

Marshaling

Marshaling our Link struct into JSON is easier than the previous example. Simply we add a MarshalJSON() method which satisfies the json.Marshaler interface. Inside of which we mimic the basic structure of our Link struct and convert everything to a primitive type that JSON supports. For time and URL we use the standard library functions for each to return a string.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func ( l Link ) MarshalJSON ( ) ( [ ] byte , error ) { basicLink := struct { ID int ` json:"id" ` Title string ` json:"title" ` URL string ` json:"url" ` CreatedAt string ` json:"createdat" ` } { ID : l . ID , Title : l . Title , URL : l . URL . String ( ) , CreatedAt : l . CreatedAt . Format ( time . RFC3339 ) , } return json . Marshal ( basicLink ) }

Marsheling our example is more direct than Unmarshaling, but still gives us the control we need to override the layout used by json.Marshal() and format time as we require it. This will also make things easier for you when working with large complex data structures that need to be output as JSON.

Go and JSON

JSON inherited some of the wonkyness from Javascript, but with a few lines of code we were able to wrangle the primitive types into something useful in our Go application. The Marshaler and Unmarshaler interfaces make it easy to hook your custom code into the standard JSON marshaling process. The examples here are a simple version of what you will need to do for your own data types.