32

What is the idiomatic way to unmarshal into time.Duration in Go? How can I make use of time.ParseDuration?

  • For types defined outside of your project that do not implement the Unmarshaler interface, or do but in a way that is not sufficient for your requirements, you should define a "wrapper" type and have it implement the Unmarshaler interface so that it meets your needs. – mkopriva Jan 01 '18 at 16:17

2 Answers2

49

The lack of JSON marshaling and unmarshaling methods on time.Duration was an unfortunate oversight. This should hopefully be resolved in Go2 (see issue #10275).

You can, however, define your own type around time.Duration that supports marshaling to the string representation of the duration and unmarshaling from either the numeric or string representations. Here is an example of such an implementation:

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "time"
)

type Duration struct {
    time.Duration
}

func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String())
}

func (d *Duration) UnmarshalJSON(b []byte) error {
    var v interface{}
    if err := json.Unmarshal(b, &v); err != nil {
        return err
    }
    switch value := v.(type) {
    case float64:
        d.Duration = time.Duration(value)
        return nil
    case string:
        var err error
        d.Duration, err = time.ParseDuration(value)
        if err != nil {
            return err
        }
        return nil
    default:
        return errors.New("invalid duration")
    }
}

type Message struct {
    Elapsed Duration `json:"elapsed"`
}

func main() {
    msgEnc, err := json.Marshal(&Message{
        Elapsed: Duration{time.Second * 5},
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s\n", msgEnc)

    var msg Message
    if err := json.Unmarshal([]byte(`{"elapsed": "1h"}`), &msg); err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", msg)
}

https://play.golang.org/p/Zm6hpNR-ZJ2

  • 1
    Why is it float64 not int64? – SOFe Jul 07 '18 at 10:37
  • 3
    Because the JSON package unmarshals numbers into `interface{}` as `float64`. –  Jul 07 '18 at 10:49
  • 1
    why is `Duration` a `struct` of `time.Duration` instead of as in the other answer: i.e. `type Duration time.Duration`? Thank you in advance :) – Nick Brady Mar 20 '19 at 00:36
  • One thing I noticed when implementing this solution but with a type alias was that you lose some of the access to existing methods on the `time.Duration` type. So one example was that when implementing `MarshalJSON` the `String` method no longer existed. – James Durand Sep 09 '19 at 01:31
29

Just to extend the previous answer. There is another way (very close to Tim's)

type Duration time.Duration 

func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Duration(d).String())
}

func (d *Duration) UnmarshalJSON(b []byte) error {
    var v interface{}
    if err := json.Unmarshal(b, &v); err != nil {
        return err
    }
    switch value := v.(type) {
    case float64:
        *d = Duration(time.Duration(value))
        return nil
    case string:
        tmp, err := time.ParseDuration(value)
        if err != nil {
            return err
        }
        *d = Duration(tmp)
        return nil
    default:
        return errors.New("invalid duration")
    }
}
Marcel
  • 1,509
  • 1
  • 17
  • 39