{"record":{"id":"a371038e028d45a5","repo":"siyuan-note/siyuan","slug":"s-must-be-a-string","errorCode":null,"errorMessage":"%s must be a string","messagePattern":"(.+?) must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view_key_config.go","lineNumber":46,"sourceCode":"func UpdateAttributeViewKeyConfig(avID, keyID string, config map[string]any) (err error) {\n\tif 1 != len(config) {\n\t\treturn errors.New(\"config must contain exactly one setting; use separate calls for multiple settings\")\n\t}\n\tattrView, err := av.ParseAttributeView(avID)\n\tif nil != err {\n\t\treturn err\n\t}\n\tkey, err := attrView.GetKey(keyID)\n\tif nil != err {\n\t\treturn err\n\t}\n\top := &Operation{AvID: avID, ID: keyID, Typ: string(key.Type), Name: key.Name}\n\tfor setting, value := range config {\n\t\tswitch setting {\n\t\tcase \"name\", \"type\", \"icon\", \"desc\", \"numberFormat\", \"dateFormat\", \"template\", \"renderTemplate\":\n\t\t\ttext, ok := value.(string)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"%s must be a string\", setting)\n\t\t\t}\n\t\t\top.Data, op.Format = text, text\n\t\t\tswitch setting {\n\t\t\tcase \"name\":\n\t\t\t\tif \"\" == strings.TrimSpace(text) {\n\t\t\t\t\treturn errors.New(\"field name must not be empty\")\n\t\t\t\t}\n\t\t\t\top.Name = text\n\t\t\t\treturn updateAttributeViewColumn(op)\n\t\t\tcase \"type\":\n\t\t\t\tif _, err = newAttributeViewKey(keyID, key.Name, text, key.Icon, key.DateFormat); nil != err {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\top.Typ = text\n\t\t\t\treturn updateAttributeViewColumn(op)\n\t\t\tcase \"icon\":\n\t\t\t\treturn setAttributeViewColIcon(op)\n\t\t\tcase \"desc\":","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view_key_config.go#L28-L64","documentation":"The string-valued settings of UpdateAttributeViewKeyConfig (\"name\", \"type\", \"icon\", \"desc\", \"numberFormat\", \"dateFormat\", \"template\", \"renderTemplate\") require the config value to be a JSON string. A non-string value (number, bool, object, null) is rejected with this error naming the offending setting.","triggerScenarios":"Sending {\"dateFormat\": 3} or {\"icon\": null} or {\"numberFormat\": {\"fmt\": \"2\"}} to the key-config update endpoint.","commonSituations":"Numeric format codes passed instead of their string names; null used to mean 'clear'; config values parsed from YAML/JSON without string coercion.","solutions":["Pass the value as a string, e.g. {\"numberFormat\": \"2\"} not {\"numberFormat\": 2}","Coerce numeric/enum values with String(value) or the equivalent before sending","Use the documented format-name strings for numberFormat/dateFormat rather than numeric codes"],"exampleFix":"// before\n{\"numberFormat\": 2}\n// after\n{\"numberFormat\": \"2\"}","handlingStrategy":"type-guard","validationCode":"const STRING_SETTINGS = ['name','type','icon','desc','numberFormat','dateFormat','template','renderTemplate'];\nfunction assertStringValues(config) { for (const [k, v] of Object.entries(config)) if (STRING_SETTINGS.includes(k) && typeof v !== 'string') throw new Error(`${k} must be a string`); }","typeGuard":"const isString = (v) => typeof v === 'string';","tryCatchPattern":"try { await updateKeyConfig(avID, keyID, cfg); } catch (e) { if (/.+ must be a string$/.test(e.message)) { const [k] = Object.keys(cfg); return updateKeyConfig(avID, keyID, { [k]: String(cfg[k]) }); } throw e; }","preventionTips":["Always stringify numeric format codes before sending","Avoid null to mean 'clear'; send an explicit string or use the dedicated clear operation","Type-check config payloads in your client wrapper"],"tags":["api","type-mismatch","validation","attribute-view"],"backgroundTag":"type-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}