Documentation
¶
Overview ¶
Package jsonduration gives time.Duration a JSON representation in encoding/json/v2.
encoding/json/v2 has no default representation for time.Duration (https://go.dev/issue/71631): marshaling or unmarshaling one returns an error. The `format` struct tag that used to select one was removed in Go 1.27 (https://go.dev/issue/79071), and its replacement depends on typed struct tags (https://go.dev/issue/74472). This package fills the gap in two ways. It is for encoding/json/v2 only.
One format for a whole call ¶
With returns options that apply one format to every time.Duration in a call: struct fields, pointers, slices, arrays, map keys and values, and values held in an any when marshaling. Your types stay unchanged.
opts := jsonduration.With(jsonduration.ISO8601)
b, err := json.Marshal(v, opts) // {"ttl":"PT1H30M"}
err = json.Unmarshal(b, &v, opts)
One format per field ¶
Each format has a wrapper type, such as ISO8601Duration or SecDuration, that embeds a time.Duration and encodes itself through the encoding/json/v2 interfaces MarshalerTo and UnmarshalerFrom. Wrapper types need no options, and With does not change their format.
type Config struct {
Timeout jsonduration.ISO8601Duration `json:"timeout"`
Backoff jsonduration.SecDuration `json:"backoff"`
}
Formats ¶
The six formats encoding/json/v2 offered through `format` are supported with the same names and identical output: Units, Sec, Milli, Micro, Nano and ISO8601. Nano is byte-for-byte compatible with encoding/json (v1). Two more are supported: Base60 from the original v2 proposal, and ProtoJSON, the protobuf JSON mapping of google.protobuf.Duration.
A Format can come from a JSON configuration or from flags: it implements encoding.TextUnmarshaler and rejects anything but the exact names. Check a Format converted from a plain string with Format.Valid.
Strictness ¶
Decoding accepts only the configured format. To read data written in other formats as well, for example while migrating, name them with AlsoAccept. ISO8601 input with nominal units (years, months, weeks, days) is rejected with ErrNominalUnits, since those have no fixed length; where a producer means 24 hours by a day, AcceptDays reads days and weeks. Fractional digits below one nanosecond are truncated toward zero, exactly, in every format. Errors wrap strconv.ErrSyntax, strconv.ErrRange or ErrNominalUnits.
JSON null sets a duration to zero, as encoding/json/v2 does for other types.
Good to know ¶
Named types such as `type TTL time.Duration` are distinct types and are not affected by With. To omit zero durations, use the `omitzero` tag option: in v2, `omitempty` only omits values that encode as empty JSON. The `string` tag option quotes numeric formats, like json.StringifyNumbers, and has no effect on string formats.
Example ¶
package main
import (
"encoding/json/v2"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
type Config struct {
Timeout time.Duration `json:"timeout"`
Retry []time.Duration `json:"retry"`
Grace *time.Duration `json:"grace,omitzero"`
}
opts := jsonduration.With(jsonduration.ISO8601)
b, err := json.Marshal(Config{
Timeout: 90 * time.Second,
Retry: []time.Duration{time.Second, 2500 * time.Millisecond},
}, opts)
fmt.Println(string(b), err)
var c Config
err = json.Unmarshal([]byte(`{"timeout":"PT2M","retry":["PT0.5S"]}`), &c, opts)
fmt.Println(c.Timeout, c.Retry, err)
}
Output: {"timeout":"PT1M30S","retry":["PT1S","PT2.5S"]} <nil> 2m0s [500ms] <nil>
Index ¶
- Variables
- func Marshalers(f Format) *json.Marshalers
- func Unmarshalers(f Format, opts ...DecodeOption) *json.Unmarshalers
- func With(f Format, opts ...DecodeOption) json.Options
- type Base60Duration
- type DecodeOption
- type Format
- func (f Format) Append(b []byte, d time.Duration) []byte
- func (f Format) Format(d time.Duration) string
- func (f Format) IsNumeric() bool
- func (f Format) MarshalText() ([]byte, error)
- func (f Format) Parse(s string) (time.Duration, error)
- func (f *Format) UnmarshalText(b []byte) error
- func (f Format) Valid() bool
- type ISO8601Duration
- type MicroDuration
- type MilliDuration
- type NanoDuration
- type ProtoJSONDuration
- type SecDuration
- type UnitsDuration
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNominalUnits = errors.New("nominal year, month, week, or day units")
ErrNominalUnits reports an ISO 8601 duration that uses the year, month, week or day designators. Their length in time is not fixed, so they cannot be converted to a time.Duration exactly. AcceptDays allows days and weeks by fixing their length.
Functions ¶
func Marshalers ¶
func Marshalers(f Format) *json.Marshalers
Marshalers returns the marshaler for time.Duration in format f. It panics if f is not valid.
Example ¶
Combine the duration marshaler with your own through json.JoinMarshalers, because a second json.WithMarshalers option would replace the first.
package main
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
type UserID int
ids := json.MarshalToFunc(func(enc *jsontext.Encoder, id UserID) error {
return enc.WriteToken(jsontext.String(fmt.Sprintf("user-%d", id)))
})
opts := json.WithMarshalers(json.JoinMarshalers(ids, jsonduration.Marshalers(jsonduration.Units)))
b, err := json.Marshal(struct {
ID UserID
TTL time.Duration
}{42, 90 * time.Minute}, opts)
fmt.Println(string(b), err)
}
Output: {"ID":"user-42","TTL":"1h30m0s"} <nil>
func Unmarshalers ¶
func Unmarshalers(f Format, opts ...DecodeOption) *json.Unmarshalers
Unmarshalers returns the unmarshaler for time.Duration in format f and in any format added with AlsoAccept. It panics if a format is not valid or if two numeric formats are involved.
Decoding is strict: only the accepted formats are tried, and a numeric format accepts a JSON string only where it would also write one (object names, or with json.StringifyNumbers). JSON null sets the duration to zero, as encoding/json/v2 does for other types.
func With ¶
func With(f Format, opts ...DecodeOption) json.Options
With returns options for encoding/json/v2 that marshal and unmarshal every time.Duration using format f: struct fields, pointers, slice and array elements, map keys and values, and values held in an any when marshaling. Decoding accepts only f, unless AlsoAccept adds formats. With panics under the same conditions as Unmarshalers.
The options are safe for concurrent use, so they can be created once, for example in a package-level variable, and shared:
var jsonOpts = jsonduration.With(jsonduration.ISO8601) b, err := json.Marshal(v, jsonOpts) err = json.Unmarshal(b, &v, jsonOpts)
If you also pass your own json.WithMarshalers or json.WithUnmarshalers, combine them with Marshalers and Unmarshalers through json.JoinMarshalers and json.JoinUnmarshalers instead, because a later WithMarshalers option replaces an earlier one.
Types ¶
type Base60Duration ¶
Base60Duration is a time.Duration that encodes itself in format Base60 in encoding/json/v2, without any options: as a JSON string such as "1:30:00.5" for 1h30m0.5s.
func (Base60Duration) MarshalJSONTo ¶
func (d Base60Duration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*Base60Duration) UnmarshalJSONFrom ¶
func (d *Base60Duration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type DecodeOption ¶
type DecodeOption interface {
// contains filtered or unexported methods
}
A DecodeOption changes how With and Unmarshalers decode. Encoding always uses the single format passed to them.
func AcceptDays ¶
func AcceptDays() DecodeOption
AcceptDays makes ISO8601 decoding through With and Unmarshalers read days as exactly 24 hours and weeks as exactly 7 days, as Java's Duration.parse does for days, so that "P1D" is 24h and "P1DT12H" is 36h. Years and months are still rejected with ErrNominalUnits, and days and weeks take no fraction. Encoding never writes days. AcceptDays has no effect on the other formats, and Format.Parse and ISO8601Duration stay strict.
Only use it where the producer means 24 hours by a day: a calendar day can be 23 or 25 hours long across a daylight saving change.
Example ¶
Read days from a service that means 24 hours by one, as Java's Duration.parse does.
package main
import (
"encoding/json/v2"
"errors"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
opts := jsonduration.With(jsonduration.ISO8601, jsonduration.AcceptDays())
var d time.Duration
err := json.Unmarshal([]byte(`"P1DT12H"`), &d, opts)
fmt.Println(d, err)
// Years and months still have no fixed length.
err = json.Unmarshal([]byte(`"P1M"`), &d, opts)
fmt.Println(errors.Is(err, jsonduration.ErrNominalUnits))
}
Output: 36h0m0s <nil> true
func AlsoAccept ¶
func AlsoAccept(formats ...Format) DecodeOption
AlsoAccept makes decoding accept the given formats in addition to the one that is written. Use it to read data written in an older format while writing a new one:
// Stored data holds v1 nanoseconds; new data is written as ISO 8601. opts := jsonduration.With(jsonduration.ISO8601, jsonduration.AlsoAccept(jsonduration.Nano))
String formats never conflict: no text is valid in two of them with different values. Where two accept the same text, such as "1.5s" in Units and ProtoJSON, they agree on its value. Numeric formats do conflict, since a JSON number such as 90 could be seconds or nanoseconds, so at most one numeric format may be involved. With and Unmarshalers panic if there are two, or if a format is not valid.
Example ¶
Read data written by encoding/json (v1), which stores nanoseconds, while writing ISO 8601 from now on.
package main
import (
"encoding/json/v2"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
type Job struct {
Timeout time.Duration `json:"timeout"`
}
opts := jsonduration.With(jsonduration.ISO8601, jsonduration.AlsoAccept(jsonduration.Nano))
for _, stored := range []string{`{"timeout":90000000000}`, `{"timeout":"PT1M30S"}`} {
var j Job
err := json.Unmarshal([]byte(stored), &j, opts)
fmt.Println(j.Timeout, err)
}
b, err := json.Marshal(Job{Timeout: 90 * time.Second}, opts)
fmt.Println(string(b), err)
}
Output: 1m30s <nil> 1m30s <nil> {"timeout":"PT1M30S"} <nil>
type Format ¶
type Format string
Format names a JSON representation of a time.Duration. Its value is the name of the format, such as "iso8601", and names match exactly, case included, as they did in the `format` struct tag option of encoding/json/v2.
A Format converted from a string, as in Format(s), is not checked. Check such a value with Format.Valid before passing it to With, which panics on an invalid format. Format.UnmarshalText checks for itself.
const ( // Units is a JSON string produced by [time.Duration.String], e.g. // "1h30m0.5s". Like the standard library, durations under one // millisecond use "µs". Input is what [time.ParseDuration] accepts, // such as "1h30m" or "1.5us", but fractions are computed exactly // instead of through float64, and errors wrap [strconv.ErrSyntax] or // [strconv.ErrRange] like those of the other formats. Units Format = "units" // Sec is a JSON number of seconds with up to 9 fractional digits, // e.g. 5400.5. It is exact: the value is never routed through float64. Sec Format = "sec" // Milli is a JSON number of milliseconds, e.g. 5400500. Milli Format = "milli" // Micro is a JSON number of microseconds, e.g. 5400500000. Micro Format = "micro" // Nano is a JSON integer of nanoseconds, e.g. 5400500000000. // This is the representation of encoding/json (v1). Nano Format = "nano" // ISO8601 is a JSON string with the restricted ISO 8601 profile of // encoding/json/v2, e.g. "PT1H30M0.5S". Output only uses the accurate // hour, minute and second designators. Input follows the grammar of // JavaScript's Temporal.Duration; unlike the standard library, input // with the nominal designators year, month, week or day is rejected, // unless [AcceptDays] allows days and weeks. ISO8601 Format = "iso8601" // Base60 is a JSON string in H:MM:SS.SSSSSSSSS notation, e.g. "1:30:00.5". // It was part of the original encoding/json/v2 proposal and was removed // after the vote in https://go.dev/issue/71631. Base60 Format = "base60" // ProtoJSON is the JSON string of google.protobuf.Duration: // seconds followed by "s", e.g. "5400.500s". Output has 0, 3, 6 or 9 // fractional digits as the protobuf JSON mapping requires; input // accepts 0 to 9 fractional digits. ProtoJSON Format = "protojson" )
func Formats ¶
func Formats() []Format
Formats returns every supported format, in a new slice on each call.
func (Format) Append ¶
Append appends the representation of d in format f to b, without JSON quoting. It panics if f is not valid.
func (Format) Format ¶
Format returns the representation of d in format f, without JSON quoting. It panics if f is not valid.
Example ¶
package main
import (
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
d := 90*time.Minute + 500*time.Millisecond
for _, f := range jsonduration.Formats() {
fmt.Printf("%-9s %s\n", f, f.Format(d))
}
}
Output: units 1h30m0.5s sec 5400.5 milli 5400500 micro 5400500000 nano 5400500000000 iso8601 PT1H30M0.5S base60 1:30:00.5 protojson 5400.500s
func (Format) IsNumeric ¶
IsNumeric reports whether f encodes as a JSON number. Numeric formats are written as JSON strings when used as an object name or when json.StringifyNumbers is set.
Example ¶
Numeric formats are written as JSON strings where JSON requires one, such as object names, and wherever json.StringifyNumbers asks for it.
package main
import (
"encoding/json/v2"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
m := map[time.Duration]time.Duration{time.Minute: time.Hour}
b, _ := json.Marshal(m, jsonduration.With(jsonduration.Sec))
fmt.Println(string(b))
b, _ = json.Marshal(m, jsonduration.With(jsonduration.Sec), json.StringifyNumbers(true))
fmt.Println(string(b))
}
Output: {"60":3600} {"60":"3600"}
func (Format) MarshalText ¶
MarshalText implements encoding.TextMarshaler, so that a format can be written to configuration. It returns an error if f is not valid.
func (Format) Parse ¶
Parse parses s, which must be in format f and not JSON quoted. Only format f is accepted; there is no fallback to other formats. Fractional digits below one nanosecond are truncated toward zero. Errors wrap strconv.ErrSyntax, strconv.ErrRange or ErrNominalUnits.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/pscheid92/jsonduration"
)
func main() {
d, err := jsonduration.ISO8601.Parse("PT1H30M")
fmt.Println(d, err)
// Days have no fixed length, so they are rejected.
_, err = jsonduration.ISO8601.Parse("P1D")
fmt.Println(errors.Is(err, jsonduration.ErrNominalUnits))
// Only the configured format is accepted.
_, err = jsonduration.ISO8601.Parse("1h30m")
fmt.Println(err)
}
Output: 1h30m0s <nil> true invalid ISO 8601 duration "1h30m": invalid syntax
func (*Format) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler, so that a format in a JSON configuration is checked while encoding/json/v2 decodes it, and so that a format can come from a flag (see flag.TextVar). It accepts exactly the names of the formats, such as "iso8601", and rejects anything else, including other spellings such as "ISO8601", with an error that lists the names.
Example ¶
Choose the format at run time, from a flag. The same works for a format in a JSON configuration. Names must match exactly, and anything else is rejected right away.
package main
import (
"encoding/json/v2"
"flag"
"fmt"
"io"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
fs := flag.NewFlagSet("server", flag.ContinueOnError)
fs.SetOutput(io.Discard) // keep the usage message out of the example
var format jsonduration.Format
fs.TextVar(&format, "duration-format", jsonduration.ISO8601, "how durations are written")
_ = fs.Parse([]string{"-duration-format=sec"})
b, err := json.Marshal(90*time.Second, jsonduration.With(format))
fmt.Println(string(b), err)
fmt.Println(fs.Parse([]string{"-duration-format=weeks"}) != nil)
}
Output: 90 <nil> true
type ISO8601Duration ¶
ISO8601Duration is a time.Duration that encodes itself in format ISO8601 in encoding/json/v2, without any options: as a JSON string such as "PT1H30M0.5S" for 1h30m0.5s.
Example ¶
Wrapper types choose the format per field and need no options. The methods of time.Duration stay available.
package main
import (
"encoding/json/v2"
"fmt"
"time"
"github.com/pscheid92/jsonduration"
)
func main() {
type Config struct {
Timeout jsonduration.ISO8601Duration `json:"timeout"`
Backoff jsonduration.SecDuration `json:"backoff"`
}
b, err := json.Marshal(Config{
Timeout: jsonduration.ISO8601Duration{Duration: 90 * time.Second},
Backoff: jsonduration.SecDuration{Duration: 1500 * time.Millisecond},
})
fmt.Println(string(b), err)
var c Config
err = json.Unmarshal([]byte(`{"timeout":"PT2M","backoff":0.25}`), &c)
fmt.Println(c.Timeout.Minutes(), c.Backoff, err)
}
Output: {"timeout":"PT1M30S","backoff":1.5} <nil> 2 250ms <nil>
func (ISO8601Duration) MarshalJSONTo ¶
func (d ISO8601Duration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*ISO8601Duration) UnmarshalJSONFrom ¶
func (d *ISO8601Duration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type MicroDuration ¶
MicroDuration is a time.Duration that encodes itself in format Micro in encoding/json/v2, without any options: as a JSON number such as 5400500000 for 1h30m0.5s.
func (MicroDuration) MarshalJSONTo ¶
func (d MicroDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*MicroDuration) UnmarshalJSONFrom ¶
func (d *MicroDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type MilliDuration ¶
MilliDuration is a time.Duration that encodes itself in format Milli in encoding/json/v2, without any options: as a JSON number such as 5400500 for 1h30m0.5s.
func (MilliDuration) MarshalJSONTo ¶
func (d MilliDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*MilliDuration) UnmarshalJSONFrom ¶
func (d *MilliDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type NanoDuration ¶
NanoDuration is a time.Duration that encodes itself in format Nano in encoding/json/v2, without any options: as a JSON number such as 5400500000000 for 1h30m0.5s.
func (NanoDuration) MarshalJSONTo ¶
func (d NanoDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*NanoDuration) UnmarshalJSONFrom ¶
func (d *NanoDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type ProtoJSONDuration ¶
ProtoJSONDuration is a time.Duration that encodes itself in format ProtoJSON in encoding/json/v2, without any options: as a JSON string such as "5400.500s" for 1h30m0.5s.
func (ProtoJSONDuration) MarshalJSONTo ¶
func (d ProtoJSONDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*ProtoJSONDuration) UnmarshalJSONFrom ¶
func (d *ProtoJSONDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type SecDuration ¶
SecDuration is a time.Duration that encodes itself in format Sec in encoding/json/v2, without any options: as a JSON number such as 5400.5 for 1h30m0.5s.
func (SecDuration) MarshalJSONTo ¶
func (d SecDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*SecDuration) UnmarshalJSONFrom ¶
func (d *SecDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.
type UnitsDuration ¶
UnitsDuration is a time.Duration that encodes itself in format Units in encoding/json/v2, without any options: as a JSON string such as "1h30m0.5s" for 1h30m0.5s.
func (UnitsDuration) MarshalJSONTo ¶
func (d UnitsDuration) MarshalJSONTo(enc *jsontext.Encoder) error
MarshalJSONTo implements encoding/json/v2.MarshalerTo.
func (*UnitsDuration) UnmarshalJSONFrom ¶
func (d *UnitsDuration) UnmarshalJSONFrom(dec *jsontext.Decoder) error
UnmarshalJSONFrom implements encoding/json/v2.UnmarshalerFrom.