{"record":{"id":"eecc679b779f6d00","repo":"hashicorp/nomad","slug":"unexpected-expirationleeway-type-v-eecc67","errorCode":null,"errorMessage":"unexpected ExpirationLeeway type: %v","messagePattern":"unexpected ExpirationLeeway type: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/structs/acl.go","lineNumber":1678,"sourceCode":"\t\t*Alias\n\t}{\n\t\tAlias: (*Alias)(a),\n\t}\n\tif err = json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\tif aux.ExpirationLeeway != nil {\n\t\tswitch v := aux.ExpirationLeeway.(type) {\n\t\tcase string:\n\t\t\tif v != \"\" {\n\t\t\t\tif a.ExpirationLeeway, err = time.ParseDuration(v); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\tcase float64:\n\t\t\ta.ExpirationLeeway = time.Duration(v)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected ExpirationLeeway type: %v\", v)\n\t\t}\n\t}\n\tif aux.NotBeforeLeeway != nil {\n\t\tswitch v := aux.NotBeforeLeeway.(type) {\n\t\tcase string:\n\t\t\tif v != \"\" {\n\t\t\t\tif a.NotBeforeLeeway, err = time.ParseDuration(v); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\tcase float64:\n\t\t\ta.NotBeforeLeeway = time.Duration(v)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected NotBeforeLeeway type: %v\", v)\n\t\t}\n\t}\n\tif aux.ClockSkewLeeway != nil {\n\t\tswitch v := aux.ClockSkewLeeway.(type) {","sourceCodeStart":1660,"sourceCodeEnd":1696,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/structs/acl.go#L1660-L1696","documentation":"During JSON unmarshalling of ACLAuthMethod (custom UnmarshalJSON), ExpirationLeeway accepts only a string duration (\"5m\") or a JSON number (float64, treated as nanoseconds); any other JSON type (bool, object, array, null-like leftover) hits the default branch and returns this error.","triggerScenarios":"Posting auth-method JSON where ExpirationLeeway is e.g. true, an object {\"seconds\":300}, or an array — anything that is neither a string nor number.","commonSituations":"Hand-writing JSON config and quoting wrong / nesting durations; tools emitting YAML that maps to maps rather than scalar durations; forgetting Go's duration-string format and using \"5 minutes\".","solutions":["Send ExpirationLeeway as a Go duration string like \"5m\" or \"1h30m\".","Or send a bare JSON number interpreted as nanoseconds (300000000000 for 5m).","Fix the client/SDK serialization to emit a scalar for this field."],"exampleFix":"// before\n{\"ExpirationLeeway\": {\"minutes\": 5}}\n// after\n{\"ExpirationLeeway\": \"5m\"}","handlingStrategy":"validation","validationCode":"switch v := raw[\"ExpirationLeeway\"].(type) {\ncase string, float64:\n\t// ok\ndefault:\n\treturn fmt.Errorf(\"ExpirationLeeway must be a duration string or number, got %T\", v)\n}","typeGuard":"func isDurationScalar(v interface{}) bool {\n\tswitch v.(type) {\n\tcase string, float64, float32, int64:\n\t\treturn true\n\t}\n\treturn false\n}","tryCatchPattern":"if err := json.Unmarshal(payload, &am); err != nil {\n\tif strings.Contains(err.Error(), \"unexpected ExpirationLeeway type\") {\n\t\treturn fmt.Errorf(\"send ExpirationLeeway as e.g. \\\"5m\\\" or a nanosecond number\")\n\t}\n\treturn err\n}","preventionTips":["Always encode leeways as Go duration strings ('5m', '1h30m') in JSON","Reject nested duration objects in config renderers","Number form is nanoseconds — convert seconds deliberately"],"tags":["json","unmarshal","acl","nomad"],"backgroundTag":"json-unmarshal-type-mismatch","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}