siyuan-note/siyuan · error
legacy providerConfig JSON decode error with type names…
Error message
legacy providerConfig JSON decode error with type names remapped (SettingProvider. to Provider., SettingModel. to Model.)
What it means
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.
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
Example fix
// before
fetchPost("/api/ai/saveProvider", {provider: "OpenAI", providerConfig: {apiKey: 12345}});
// after
fetchPost("/api/ai/saveProvider", {provider: "OpenAI", providerConfig: {apiKey: "sk-12345"}}); Defensive patterns
Strategy: validation
Validate before calling
function isValidProviderConfig(cfg) { return cfg == null || (typeof cfg.apiKey === "string" && (!cfg.models || Array.isArray(cfg.models))); } Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- element [ ]
- empty JSON value
- entry [ ]
- failed to serialize inputSchema
- Field [conf] should be of type [Object]
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/b595ae8a12ea9a34.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/apicontract/ai_input.go:55
}
func aiJSONDecoder[Request, Data any](endpoint *Endpoint[Request, Data], bind func(fileTreeFields) (Request, error)) {
endpoint.decodeRequest = func(reader io.Reader) (value Request, err error) {
fields, err := fileTreeRequestFields(reader, endpoint.definition.Path)
if err == nil {
value, err = bind(fields)
}
return
}
}
func aiProviderFields(fields fileTreeFields) AIProviderRequest {
request := AIProviderRequest{}
_ = json.Unmarshal(fields["provider"], &request.Provider)
if raw := fields["providerConfig"]; len(raw) > 0 && !bytes.Equal(raw, []byte("null")) {
value, err := legacyJSONValue[SettingProvider](raw)
if err != nil {
err = fmt.Errorf("%s", strings.NewReplacer("SettingProvider.", "Provider.", "SettingModel.", "Model.", "apicontract.SettingProvider", "conf.Provider", "apicontract.SettingModel", "conf.Model").Replace(err.Error()))
}
request.ProviderConfig = &value
request.providerError = err
}
return request
}
func init() {
aiStructDecoder(&AIEditorChat, "aiEditorChatReq")
aiStructDecoder(&AIAgentChat, "agentChatReq")
aiStructDecoder(&AIGetSession, "agentSessionGetReq")
AISaveSession.decodeRequest = func(reader io.Reader) (value AISession, err error) {
value.raw, err = io.ReadAll(reader)
if err != nil {
err = fmt.Errorf("failed to read body: %s", err)
}
return
}View on GitHub (pinned to 9f775e8a12)