grpc/grpc-go · error
malformed duration : too many decimals
Error message
malformed duration %q: too many decimals
What it means
After stripping the trailing 's' and an optional leading '-', UnmarshalJSON splits the body on '.' with a limit of 3 (duration.go:74-77). If there is more than one '.' (i.e. SplitN yields more than 2 parts), the duration is rejected as having too many decimal points. The protobuf-JSON duration allows at most one decimal separator between whole and fractional seconds.
Solutions
- Use a single decimal point: "1.25s", "0.500s".
- Avoid thousands separators in durations.
- Validate the string with regex '^-?\d+(\.\d{1,9})?s$' before submitting.
Example fix
// before:
// { "retryPolicy": { "initialBackoff": "0.5.5s" } }
// // error: malformed duration "0.5.5s": too many decimals
// after:
// { "retryPolicy": { "initialBackoff": "0.500s" } } Defensive patterns
Strategy: validation
Validate before calling
package main
import (
"fmt"
"regexp"
)
var durationRE319 = regexp.MustCompile(`^-?\d+(\.\d{1,9})?s$`)
func validateDuration(s string) error {
if !durationRE319.MatchString(s) {
return fmt.Errorf("malformed duration %q: use one decimal point, e.g. \"0.5s\"", s)
}
return nil
}
// func main() { _ = validateDuration("0.500s") } Try / catch
// Unmarshal into serviceconfig.Duration surfaces this error.
//
// var d serviceconfig.Duration
// if err := json.Unmarshal([]byte(`"`+raw+`"`), &d); err != nil {
// if strings.Contains(err.Error(), "too many decimals") {
// // collapse to a single decimal point and retry.
// }
// } Prevention
- Allow at most one '.' in a duration string.
- Generate duration strings from time.Duration via the protobuf JSON conventions.
- Reject locale-specific separators (comma) and thousands separators before serialization.
When it happens
Trigger: Triggered when a duration string contains two or more '.' characters, e.g. "1.2.3s", "0.5.6s", or "1..5s". The split at line 74 yields len(ss) > 2.
Common situations: Typo in hand-written service config, locale/serialization producing extra decimal points, or a value formatted with both a thousands separator and a decimal separator.
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 : missing seconds unit
- unmarshal error
- decode error
- delegating_resolver: failed to determine proxy URL for…
- delegating_resolver: unable to build the resolver for target
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/6c06fe79dd6fff6a.
Report an issue: GitHub.
Appendix: source
Thrown at internal/serviceconfig/duration.go:76
}
// 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)
}
// Maximum seconds value per the durationpb spec.
const maxProtoSeconds = 315_576_000_000
if sec > maxProtoSeconds {
return fmt.Errorf("out of range: %q", s)
}
hasDigits = true
}
if len(ss) == 2 && len(ss[1]) > 0 {View on GitHub (pinned to 0c51461d27)