{"record":{"id":"ac5173c48b2bba16","repo":"grpc/grpc-go","slug":"malformed-duration-q-missing-seconds-unit","errorCode":null,"errorMessage":"malformed duration %q: missing seconds unit","messagePattern":"malformed duration %q: missing seconds unit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/serviceconfig/duration.go","lineNumber":67,"sourceCode":"\t}\n\n\t// Generated output always contains 0, 3, 6, or 9 fractional digits,\n\t// depending on required precision.\n\tstr := fmt.Sprintf(\"%s%d.%09d\", sign, sec, ns)\n\tstr = strings.TrimSuffix(str, \"000\")\n\tstr = strings.TrimSuffix(str, \"000\")\n\tstr = strings.TrimSuffix(str, \".000\")\n\treturn []byte(fmt.Sprintf(\"\\\"%ss\\\"\", str)), nil\n}\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)","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/serviceconfig/duration.go#L49-L85","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{\"timeout\": \"1500\"}\n\n// after\n{\"timeout\": \"1.500s\"}","handlingStrategy":"validation","validationCode":"func validateProtoDurationJSON(b []byte) error {\n    var s string\n    if err := json.Unmarshal(b, &s); err != nil { return err }\n    if !strings.HasSuffix(s, \"s\") {\n        return fmt.Errorf(\"duration %q missing trailing 's'\", s)\n    }\n    var d serviceconfig.Duration\n    return d.UnmarshalJSON(b)\n}","typeGuard":"func isProtoDurationString(s string) bool {\n    if !strings.HasSuffix(s, \"s\") { return false }\n    var d serviceconfig.Duration\n    return d.UnmarshalJSON([]byte(`\"`+s+`\"`)) == nil\n}","tryCatchPattern":"if err := json.Unmarshal(cfg, &sc); err != nil {\n    if strings.Contains(err.Error(), \"missing seconds unit\") {\n        // surface a config-level error pointing at the offending field\n    }\n    return err\n}","preventionTips":["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."],"tags":["grpc","service-config","duration","json-parsing","configuration"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}