{"record":{"id":"6c06fe79dd6fff6a","repo":"grpc/grpc-go","slug":"malformed-duration-q-too-many-decimals","errorCode":null,"errorMessage":"malformed duration %q: too many decimals","messagePattern":"malformed duration %q: too many decimals","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":76,"sourceCode":"}\n\n// UnmarshalJSON unmarshals b as a duration JSON string into d.\nfunc (d *Duration) UnmarshalJSON(b []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn err\n\t}\n\tif !strings.HasSuffix(s, \"s\") {\n\t\treturn fmt.Errorf(\"malformed duration %q: missing seconds unit\", s)\n\t}\n\tneg := false\n\tif s[0] == '-' {\n\t\tneg = true\n\t\ts = s[1:]\n\t}\n\tss := strings.SplitN(s[:len(s)-1], \".\", 3)\n\tif len(ss) > 2 {\n\t\treturn fmt.Errorf(\"malformed duration %q: too many decimals\", s)\n\t}\n\t// hasDigits is set if either the whole or fractional part of the number is\n\t// present, since both are optional but one is required.\n\thasDigits := false\n\tvar sec, ns int64\n\tif len(ss[0]) > 0 {\n\t\tvar err error\n\t\tif sec, err = strconv.ParseInt(ss[0], 10, 64); err != nil {\n\t\t\treturn fmt.Errorf(\"malformed duration %q: %v\", s, err)\n\t\t}\n\t\t// Maximum seconds value per the durationpb spec.\n\t\tconst maxProtoSeconds = 315_576_000_000\n\t\tif sec > maxProtoSeconds {\n\t\t\treturn fmt.Errorf(\"out of range: %q\", s)\n\t\t}\n\t\thasDigits = true\n\t}\n\tif len(ss) == 2 && len(ss[1]) > 0 {","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/serviceconfig/duration.go#L58-L94","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before:\n//   { \"retryPolicy\": { \"initialBackoff\": \"0.5.5s\" } }\n//   // error: malformed duration \"0.5.5s\": too many decimals\n\n// after:\n//   { \"retryPolicy\": { \"initialBackoff\": \"0.500s\" } }","handlingStrategy":"validation","validationCode":"package main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n)\n\nvar durationRE319 = regexp.MustCompile(`^-?\\d+(\\.\\d{1,9})?s$`)\n\nfunc validateDuration(s string) error {\n\tif !durationRE319.MatchString(s) {\n\t\treturn fmt.Errorf(\"malformed duration %q: use one decimal point, e.g. \\\"0.5s\\\"\", s)\n\t}\n\treturn nil\n}\n\n// func main() { _ = validateDuration(\"0.500s\") }","typeGuard":null,"tryCatchPattern":"// Unmarshal into serviceconfig.Duration surfaces this error.\n//\n//   var d serviceconfig.Duration\n//   if err := json.Unmarshal([]byte(`\"`+raw+`\"`), &d); err != nil {\n//       if strings.Contains(err.Error(), \"too many decimals\") {\n//           // collapse to a single decimal point and retry.\n//       }\n//   }","preventionTips":["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."],"tags":["serviceconfig","duration","json","parsing","configuration","grpc"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}