3 minutes
Go: Unmarshalling JSON into time.Duration
Background
Continuing with the theme of the last post, exploring Go and JSON unmarshalling, I thought I would write about unmarshalling time.Duration
.
Go’s encoding/json
package supports unmarshalling JSON into the following types:
bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null
With that in mind, how can you handle other types like time.Duration
?
What is Special About “time.Duration”?
In the same vein as the last post, the solution relies on a custom struct and use Go’s UnmarshalJSON()
interface
. time.Duration
presents an extra challenge because it can be represented by a string or a number.
time.Duration
is used to store the elapsed time between two points in time. This value is stored natively in nanoseconds as an int64
.
A string representation of 1 second is "1s"
. To store "1s"
as a time.Duration
, we can write:
duration, _ := time.ParseDuration("1s")
// Prints "Duration: 1000000000 nanoseconds"
fmt.Printf("Duration: %#v nanoseconds\n", duration)
Equally, we could accept a value of 5000000000
nanoseconds (5 seconds):
duration = time.Duration(5 * time.Second)
// Prints "Duration: 5 seconds"
fmt.Printf("Duration: %.0f seconds", duration.Seconds())
Go Playground example.
If we want to retain this flexibility when unmarshalling a JSON string, our custom UnmarshalJSON()
method has to be able to handle both the string
and float64
types. Note that these are two of the types natively supported by json.Unmarshal()
.
Custom Struct Type and UnmarshalJSON() Method
Before writing a custom UnmarshalJSON()
method, create the custom struct
type:
type Duration struct {
time.Duration
}
With that formality undertaken, go ahead and create the unmarshaller:
func (duration *Duration) UnmarshalJSON(b []byte) error {
var unmarshalledJson interface{}
err := json.Unmarshal(b, &unmarshalledJson)
if err != nil {
return err
}
switch value := unmarshalledJson.(type) {
case float64:
duration.Duration = time.Duration(value)
case string:
duration.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
default:
return fmt.Errorf("invalid duration: %#v", unmarshalledJson)
}
return nil
}
Although this unmarshaller is not disimilar to the NullBool
unmarshaller we wrote last time, there are some differences to unpack.
Notice that instead of unmarshalling directly into a specific type, the code unmarshalls into an interface{}
. Empty interfaces in Go can hold any type.
With the JSON unmarshalled, we want to populate the Duration
struct. If the supplied JSON contains a float64
, it can be cast to time.Duration
. If the supplied JSON contains a string
, it can be parsed with time.ParseDuration()
.
To determine whether we should be casting or parsing the supplied value, the special .(type)
operator is employed. .(type)
returns the underlying concrete type of an interface{}
.
Idiosyncratically the Go compiler insists that .(type)
can only be used in a switch
statement.
Using the Unmarshaller
Create a struct
to represent the JSON data:
type JsonInput struct {
Id int
Author string
ReadingTime Duration
}
Then, unmarshal as usual:
jsonData := []byte(`
{
"Id": 0,
"Author": "Catriona Trottenbrow",
"ReadingTime": 300000000000
}`)
var input = JsonInput{}
err := json.Unmarshal(jsonData, &input)
if err != nil {
fmt.Print(err)
}
Conclusion
Click here for the Go Playground example.
Go 21 will make it unnecessary to use custom struct
types and UnmarshalJSON()
methods to unmarshal time.Duration
.
This technique will no doubt be useful in the meantime with time.Duration
and other special data types.
References
- pkg.go.dev encoding/json#Unmarshal
- pkg.go.dev time
- The Go Programming Language Specification - Type switches