{"record":{"id":"90c5592e5ed71d17","repo":"siyuan-note/siyuan","slug":"field-rootids-should-be-of-type-array","errorCode":null,"errorMessage":"Field [rootIDs] should be of type [Array]","messagePattern":"Field \\[rootIDs\\] should be of type \\[Array\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/storage.go","lineNumber":23,"sourceCode":"\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n)\n\nfunc init() {\n\tBatchUpdateRecentDocCloseTime.decodeRequest = func(reader io.Reader) (request RecentDocsUpdateRequest, err error) {\n\t\tfields, err := blockRequestFields(reader, \"/api/storage/batchUpdateRecentDocCloseTime\")\n\t\tif err != nil {\n\t\t\treturn request, err\n\t\t}\n\t\traw := fields[\"rootIDs\"]\n\t\tif len(raw) == 0 || bytes.Equal(raw, []byte(\"null\")) {\n\t\t\treturn request, nil\n\t\t}\n\t\tvar entries []json.RawMessage\n\t\tif json.Unmarshal(raw, &entries) != nil {\n\t\t\treturn request, errors.New(\"Field [rootIDs] should be of type [Array]\")\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tvar id string\n\t\t\tif json.Unmarshal(entry, &id) == nil && id != \"\" {\n\t\t\t\trequest.RootIDs = append(request.RootIDs, id)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\tplain := PatchViewState\n\tPatchViewState.decodeRequest = func(reader io.Reader) (ViewStatePatchRequest, error) {\n\t\trequest, err := plain.Decode(reader)\n\t\tif err != nil {\n\t\t\tswitch {\n\t\t\tcase strings.HasPrefix(err.Error(), \"Field [values]\"):\n\t\t\t\terr = errors.New(\"Field [values]: should be of type [Object]\")\n\t\t\tcase strings.HasPrefix(err.Error(), \"Field [removeKeys]\"):\n\t\t\t\terr = errors.New(\"Field [removeKeys]: each element should be a non-empty String\")","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/storage.go#L5-L41","documentation":"The storage (SetWorkspaceData / rootIDs-style) decoder accepts an optional 'rootIDs' field but, if present, it must be a JSON array of strings. A non-array value (object, string, number) fails with 'Field [rootIDs] should be of type [Array]'. Non-string or empty entries are silently skipped rather than erroring.","triggerScenarios":"Calling the storage endpoint with rootIDs set to a single string, an object, or a number instead of an array of document ID strings; or rootIDs:null (which is accepted, not an error).","commonSituations":"Clients passing a single ID instead of wrapping it in an array, JS objects used as ID sets serialized as JSON objects, or older API usage where rootIDs was a comma-separated string.","solutions":["Send rootIDs as a JSON array of strings, e.g. [\"20240101120000-abcdefg\"]","Wrap a single ID: rootIDs: [id] instead of rootIDs: id","Pass rootIDs:null or omit it to indicate 'all'","Filter out null/undefined entries before serializing"],"exampleFix":"// before\nbody: JSON.stringify({ rootIDs: '20240101120000-abcdefg' })\n// after\nbody: JSON.stringify({ rootIDs: ['20240101120000-abcdefg'] })","handlingStrategy":"type-guard","validationCode":"function assertRootIDs(v: unknown): string[] {\n  if (v === null || v === undefined) return [];\n  if (!Array.isArray(v)) throw new Error('rootIDs must be an array of string IDs');\n  return v.filter((id): id is string => typeof id === 'string' && id !== '');\n}","typeGuard":"function isStringArray(v: unknown): v is string[] { return Array.isArray(v) && v.every((x) => typeof x === 'string'); }","tryCatchPattern":"try { await setStorage(data); } catch (e) { if (String(e).includes('rootIDs') && String(e).includes('Array')) { data.rootIDs = [].concat(data.rootIDs); retry(); } }","preventionTips":["Always wrap a single ID in an array before sending","Filter null/undefined/empty entries out of the array","Type rootIDs as string[] in client code","Pass null or omit rootIDs to mean 'no filter'"],"tags":["validation","type-mismatch","json","array"],"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"}