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

  1. Validate providerConfig against the current conf.Provider schema before sending
  2. Fix the mismatched field type indicated by the remapped message path (Provider.<field> or Model.<field>)
  3. Omit providerConfig (or send null) if you do not intend to change provider settings, so decoding is skipped
  4. 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

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


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)