{"record":{"id":"b595ae8a12ea9a34","repo":"siyuan-note/siyuan","slug":"legacy-providerconfig-json-decode-error-with-type-names","errorCode":null,"errorMessage":"legacy providerConfig JSON decode error with type names remapped (SettingProvider. to Provider., SettingModel. to Model.)","messagePattern":"legacy providerConfig JSON decode error with type names remapped \\(SettingProvider\\. to Provider\\., SettingModel\\. to Model\\.\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/ai_input.go","lineNumber":55,"sourceCode":"}\n\nfunc aiJSONDecoder[Request, Data any](endpoint *Endpoint[Request, Data], bind func(fileTreeFields) (Request, error)) {\n\tendpoint.decodeRequest = func(reader io.Reader) (value Request, err error) {\n\t\tfields, err := fileTreeRequestFields(reader, endpoint.definition.Path)\n\t\tif err == nil {\n\t\t\tvalue, err = bind(fields)\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc aiProviderFields(fields fileTreeFields) AIProviderRequest {\n\trequest := AIProviderRequest{}\n\t_ = json.Unmarshal(fields[\"provider\"], &request.Provider)\n\tif raw := fields[\"providerConfig\"]; len(raw) > 0 && !bytes.Equal(raw, []byte(\"null\")) {\n\t\tvalue, err := legacyJSONValue[SettingProvider](raw)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"%s\", strings.NewReplacer(\"SettingProvider.\", \"Provider.\", \"SettingModel.\", \"Model.\", \"apicontract.SettingProvider\", \"conf.Provider\", \"apicontract.SettingModel\", \"conf.Model\").Replace(err.Error()))\n\t\t}\n\t\trequest.ProviderConfig = &value\n\t\trequest.providerError = err\n\t}\n\treturn request\n}\n\nfunc init() {\n\taiStructDecoder(&AIEditorChat, \"aiEditorChatReq\")\n\taiStructDecoder(&AIAgentChat, \"agentChatReq\")\n\taiStructDecoder(&AIGetSession, \"agentSessionGetReq\")\n\tAISaveSession.decodeRequest = func(reader io.Reader) (value AISession, err error) {\n\t\tvalue.raw, err = io.ReadAll(reader)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"failed to read body: %s\", err)\n\t\t}\n\t\treturn\n\t}","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/ai_input.go#L37-L73","documentation":"When decoding the legacy 'providerConfig' JSON for AI provider endpoints, apicontract decodes into the internal SettingProvider type; if that unmarshal fails, the error message is rewritten so internal type names (SettingProvider./SettingModel.) appear as public names (Provider./Model., conf.Provider/conf.Model). The error is stored on the request (providerError) rather than aborting the call. It means the providerConfig JSON blob did not match the expected provider settings schema.","triggerScenarios":"Sending /api/ai/* provider requests (e.g. saveProvider) whose 'providerConfig' JSON contains fields with wrong types (e.g. \"apiKey\": 123 instead of a string, \"models\" as an object instead of array) or unknown nesting that cannot unmarshal into SettingProvider.","commonSituations":"Hand-edited or API-generated provider config from older SiYuan versions whose schema changed, third-party tools writing provider settings, JSON.stringify of a partially-shaped provider object.","solutions":["Validate providerConfig against the current conf.Provider schema before sending","Fix the mismatched field type indicated by the remapped message path (Provider.<field> or Model.<field>)","Omit providerConfig (or send null) if you do not intend to change provider settings, so decoding is skipped","Upgrade/downgrade the client to match the kernel's schema version"],"exampleFix":"// before\nfetchPost(\"/api/ai/saveProvider\", {provider: \"OpenAI\", providerConfig: {apiKey: 12345}});\n// after\nfetchPost(\"/api/ai/saveProvider\", {provider: \"OpenAI\", providerConfig: {apiKey: \"sk-12345\"}});","handlingStrategy":"validation","validationCode":"function isValidProviderConfig(cfg) { return cfg == null || (typeof cfg.apiKey === \"string\" && (!cfg.models || Array.isArray(cfg.models))); }","typeGuard":null,"tryCatchPattern":"try { await post(\"/api/ai/saveProvider\", payload); } catch (e) { if (/Provider\\.|Model\\./.test(e.message)) { console.error(\"providerConfig schema mismatch:\", e.message); } else throw e; }","preventionTips":["Keep provider config objects generated from the current conf.Provider schema, not hand-built","Round-trip test configs through JSON.stringify/parse to catch type drift","Send null providerConfig when you only intend to change the provider name"],"tags":["json","api","schema"],"backgroundTag":"json-unmarshal-failed","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"}