grpc/grpc-go · error
malformed duration %q: missing seconds unit
Error message
malformed duration %q: missing seconds unit
What it means
Returned by Duration.UnmarshalJSON when the JSON duration string does not end with the seconds unit 's'. This type implements the protobuf JSON Duration spec, which encodes durations as quoted strings like "3.5s" or "-1.000000001s"; the trailing 's' is mandatory and case-sensitive. Without it the parser cannot distinguish a duration from a bare number string.
Source
Thrown at internal/serviceconfig/duration.go:67
}
// Generated output always contains 0, 3, 6, or 9 fractional digits,
// depending on required precision.
str := fmt.Sprintf("%s%d.%09d", sign, sec, ns)
str = strings.TrimSuffix(str, "000")
str = strings.TrimSuffix(str, "000")
str = strings.TrimSuffix(str, ".000")
return []byte(fmt.Sprintf("\"%ss\"", str)), nil
}
// UnmarshalJSON unmarshals b as a duration JSON string into d.
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
if !strings.HasSuffix(s, "s") {
return fmt.Errorf("malformed duration %q: missing seconds unit", s)
}
neg := false
if s[0] == '-' {
neg = true
s = s[1:]
}
ss := strings.SplitN(s[:len(s)-1], ".", 3)
if len(ss) > 2 {
return fmt.Errorf("malformed duration %q: too many decimals", s)
}
// hasDigits is set if either the whole or fractional part of the number is
// present, since both are optional but one is required.
hasDigits := false
var sec, ns int64
if len(ss[0]) > 0 {
var err error
if sec, err = strconv.ParseInt(ss[0], 10, 64); err != nil {
return fmt.Errorf("malformed duration %q: %v", s, err)View on GitHub (pinned to 03255a9237)
Solutions
- Append the 's' suffix to every duration value: "500ms" becomes "0.500s" (or "0.5s").
- Express whole-second values plainly with the suffix, e.g. "10s".
- Validate config against the protobuf JSON Duration grammar (optionally signed, fractional with up to 9 digits, mandatory trailing 's').
- If generating JSON from Go, marshal through serviceconfig.Duration / protojson rather than writing raw strings.
Example fix
// before
{"timeout": "1500"}
// after
{"timeout": "1.500s"} Defensive patterns
Strategy: validation
Validate before calling
func validateProtoDurationJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil { return err }
if !strings.HasSuffix(s, "s") {
return fmt.Errorf("duration %q missing trailing 's'", s)
}
var d serviceconfig.Duration
return d.UnmarshalJSON(b)
} Type guard
func isProtoDurationString(s string) bool {
if !strings.HasSuffix(s, "s") { return false }
var d serviceconfig.Duration
return d.UnmarshalJSON([]byte(`"`+s+`"`)) == nil
} Try / catch
if err := json.Unmarshal(cfg, &sc); err != nil {
if strings.Contains(err.Error(), "missing seconds unit") {
// surface a config-level error pointing at the offending field
}
return err
} Prevention
- Marshal durations through serviceconfig.Duration or protojson rather than writing raw strings.
- Document the protobuf JSON Duration format in your config schema.
- Unit-test config files by round-tripping them through Duration.UnmarshalJSON before deploy.
When it happens
Trigger: Any gRPC service config or JSON field typed as serviceconfig.Duration whose value is missing the suffix: "3.5", "100", "1h30m" (Go time.ParseDuration style is not accepted — only the protobuf form). Triggered on json.Unmarshal of config bytes that contain such a value.
Common situations: Reusing Go time.Duration literals ("5m") in JSON config; tooling that emits numeric milliseconds; schema generators that omit the unit; migrating from time.ParseDuration-based config to protobuf JSON.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed duration %q: too many decimals
- malformed duration %q: %v
- malformed duration %q: too many digits after decimal
- malformed duration %q: contains no numbers
- out of range: %q
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/ac5173c48b2bba16.
Report an issue: GitHub.