{"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/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/serviceconfig/duration.go#L58-L94","documentation":"Returned by Duration.UnmarshalJSON when the duration string contains more than one decimal point. The number is split on '.' with a limit of 3; producing 3 parts (two dots) is invalid per the protobuf JSON Duration grammar, which permits at most one fractional component. Example: \"1.2.3s\".","triggerScenarios":"Passing a config value like \"1.5.5s\" or any duration string with multiple '.' separators. The check is `if len(ss) > 2` after strings.SplitN(s, \".\", 3) at duration.go:74.","commonSituations":"Hand-built JSON config with a typo; templating that concatenated values producing an extra dot; localized number formatting leaking into config.","solutions":["Rewrite the value with at most one decimal point, e.g. \"1.5s\".","Generate durations via serviceconfig.Duration / protojson marshal rather than hand-editing strings.","Lint duration fields for stray dots before applying service config."],"exampleFix":"// before\n{\"timeout\": \"1.5.5s\"}\n\n// after\n{\"timeout\": \"1.5s\"}","handlingStrategy":"validation","validationCode":"func validateProtoDuration(s string) error {\n    body := strings.TrimSuffix(s, \"s\")\n    if strings.Count(body, \".\") > 1 {\n        return fmt.Errorf(\"too many decimals in %q\", s)\n    }\n    var d serviceconfig.Duration\n    return d.UnmarshalJSON([]byte(`\"` + s + `\"`))\n}","typeGuard":"func hasAtMostOneDecimalPoint(s string) bool {\n    return strings.Count(strings.TrimSuffix(s, \"s\"), \".\") <= 1\n}","tryCatchPattern":null,"preventionTips":["Generate durations via serviceconfig.Duration marshaling rather than hand-editing.","Lint config duration fields for stray dots in CI."],"tags":["grpc","service-config","duration","json-parsing","validation"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}