{"record":{"id":"b7714519b2489de6","repo":"hashicorp/nomad","slug":"unexpected-expirationleeway-type-v","errorCode":null,"errorMessage":"unexpected ExpirationLeeway type: %v","messagePattern":"unexpected ExpirationLeeway type: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/acl.go","lineNumber":956,"sourceCode":"\t}{\n\t\tAlias: (*Alias)(c),\n\t}\n\tif err := json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\tvar err error\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 c.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\tc.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 c.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\tc.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":938,"sourceCodeEnd":974,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/api/acl.go#L938-L974","documentation":"In api/acl.go, when unmarshaling a bound ACL (AuthMethod/BindingRule clock skew config), the JSON field for ExpirationLeeway may arrive as a string or number. If the raw JSON value is neither string nor float64, this error is returned during custom UnmarshalJSON handling of the leeway duration.","triggerScenarios":"POST/PUT to /v1/acl/auth-method (or decoding an auth method response) where the `ExpirationLeeway` field in the JSON payload is an object, bool, null-as-non-pointer, or otherwise not a string duration or numeric nanoseconds.","commonSituations":"Hand-built JSON payloads sending `{\"ExpirationLeeway\": \"3 hours and 5 minutes\"}` or nested objects; SDK consumers marshaling a struct into the aux field with the wrong type; Go time.Duration marshaled as a struct.","solutions":["Send ExpirationLeeway as a duration string like \"5m\", \"1h30m\", or as an integer number of nanoseconds.","Fix client code so it marshals time.Duration values as strings/ints rather than objects.","Confirm the API version supports the field and the struct's custom UnmarshalJSON is invoked (not plain json.Unmarshal into a conflicting type)."],"exampleFix":"// before\n{\"ExpirationLeeway\": {\"value\": 5, \"unit\": \"m\"}}\n// after\n{\"ExpirationLeeway\": \"5m\"}","handlingStrategy":"validation","validationCode":"// ensure leeway fields are string durations before sending\nfunc validLeeway(v interface{}) bool {\n    switch t := v.(type) {\n    case string:\n        _, err := time.ParseDuration(t)\n        return err == nil\n    case float64:\n        return true\n    default:\n        return false\n    }\n}","typeGuard":"func asDurationString(v interface{}) (string, bool) {\n    s, ok := v.(string)\n    if !ok {\n        return \"\", false\n    }\n    if _, err := time.ParseDuration(s); err != nil {\n        return \"\", false\n    }\n    return s, true\n}","tryCatchPattern":null,"preventionTips":["Always serialize time.Duration fields as strings in client payloads.","Validate request JSON against the auth-method schema before POST/PUT.","Avoid re-encoding API responses through intermediate structs that change field types."],"tags":["json","unmarshal","acl","consul-api"],"backgroundTag":"json-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"}