{"record":{"id":"5e8a94e23e402f83","repo":"siyuan-note/siyuan","slug":"invalid-template-directory","errorCode":null,"errorMessage":"Invalid template directory","messagePattern":"Invalid template directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/template.go","lineNumber":40,"sourceCode":"\t\t}\n\t\tif request.ID, err = legacyField[string](fields, \"id\", \"String\", true); err != nil {\n\t\t\treturn request, err\n\t\t}\n\t\tif request.Name, err = legacyField[string](fields, \"name\", \"String\", true); err != nil {\n\t\t\treturn request, err\n\t\t}\n\t\tif request.Overwrite, err = legacyField[bool](fields, \"overwrite\", \"Boolean\", true); err != nil {\n\t\t\treturn request, err\n\t\t}\n\t\trequest.DatabaseMode = \"copy\"\n\t\tif raw, exists := fields[\"databaseMode\"]; exists {\n\t\t\tif string(raw) == \"null\" || json.Unmarshal(raw, &request.DatabaseMode) != nil {\n\t\t\t\trequest.DatabaseMode = \"invalid\"\n\t\t\t}\n\t\t}\n\t\tif raw, exists := fields[\"directory\"]; exists {\n\t\t\tif string(raw) == \"null\" || json.Unmarshal(raw, &request.Directory) != nil {\n\t\t\t\treturn request, fmt.Errorf(\"Invalid template directory\")\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n\ntype RenderSprigRequest struct {\n\tTemplate string `json:\"template\"`\n}\n\ntype TemplateDocumentRequest struct {\n\tID string `json:\"id\"`\n}\n\ntype TemplateDocumentInfo struct {\n\tName        string `json:\"name\"`\n\tDirectory   string `json:\"directory\"`\n\tHasDatabase bool   `json:\"hasDatabase\"`","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/template.go#L22-L58","documentation":"DocSaveAsTemplate (/api/template/docSaveAsTemplate) accepts an optional `directory` field that must be a JSON string. If the field is present but is null, or cannot be unmarshalled into a string, decoding fails immediately with 'Invalid template directory'. Unlike databaseMode (which falls back to a sentinel), directory is treated as a hard input error.","triggerScenarios":"POSTing {\"id\":\"...\",\"name\":\"...\",\"overwrite\":true,\"directory\":null} or directory as a number/boolean/object, e.g. {\"directory\":123}.","commonSituations":"A client serializing an undefined variable to null and including the key anyway; a UI dropdown storing a numeric directory index instead of the string name; copy/paste payloads where the directory was quoted incorrectly.","solutions":["Send directory as a plain string, e.g. {\"directory\":\"templates\"}","Omit the directory field entirely if you do not need a subdirectory","Check the client value for null/undefined before including it in the payload"],"exampleFix":"// before\n{\"id\":\"20240101120000-abc\", \"name\":\"tpl\", \"overwrite\":false, \"directory\":null}\n// after\n{\"id\":\"20240101120000-abc\", \"name\":\"tpl\", \"overwrite\":false, \"directory\":\"my-templates\"}","handlingStrategy":"validation","validationCode":"if ('directory' in p && (p.directory === null || typeof p.directory !== 'string')) throw new Error('directory must be a string');","typeGuard":"const isDirectory = (v) => v === undefined || typeof v === 'string';","tryCatchPattern":"const res = await fetchPost('/api/template/docSaveAsTemplate', payload); if (res.code !== 0) console.error(res.msg);","preventionTips":["Delete the directory key instead of sending null","Ensure UI inputs yield strings, not numbers","Omit optional fields you do not set"],"tags":["json","template","validation","type-mismatch"],"backgroundTag":"invalid-argument-format","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"}