siyuan-note/siyuan · error

value must not be null

Error message

value must not be null

What it means

decodeRequestValue in kernel/apicontract/decode.go rejects a JSON literal null for any destination that is not a pointer, map, or slice. Pointers/maps/slices accept null (set to zero value), but a plain string, number, or bool field cannot represent null, so the decoder fails fast instead of silently coercing. This keeps typed contract fields strictly typed.

Solutions

  1. Omit the field entirely instead of sending null
  2. Send a sensible zero value (empty string, false, 0) rather than null
  3. Make the field optional in your client by conditionally including it in the payload
  4. Check the contract type in kernel/apicontract/ to see which fields accept null (pointer/map/slice only)

Example fix

// before
const payload = { id: null, title: "doc" };
// after
const payload = { title: "doc" }; // omit null fields
Defensive patterns

Strategy: type-guard

Validate before calling

function stripNulls(obj) {
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== null));
}
payload = stripNulls(payload);

Type guard

const isNullableField = (kind) => ["pointer", "map", "slice"].includes(kind); // only these accept null server-side

Try / catch

try { await fetchPost(url, stripNulls(payload)); } catch (e) { if (String(e).includes("must not be null")) console.error("Non-nullable field sent as null:", e); }

Prevention

When it happens

Trigger: Posting a JSON body where a non-pointer scalar contract field (e.g. a string id, bool flag, or number) is explicitly set to null, either literally or via JSON.stringify of an object whose property is null; nested through decodeExportFields/decodeRequestFields.

Common situations: JavaScript clients building objects with null placeholders; optional fields represented as null instead of being omitted; older plugin code written against a nullable field that became non-nullable in the contract.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/9912a8f0b0eac440. Report an issue: GitHub.

Appendix: source

Thrown at kernel/apicontract/decode.go:171

				}
			}
		}
	}
	return nil
}

// decodeRequestValue 递归绑定复合参数,避免数组元素和嵌套字段绕过空值及必填检查。
func decodeRequestValue(raw json.RawMessage, value reflect.Value) error {
	if value.Type() == reflect.TypeFor[JSONValue]() || value.Type() == reflect.TypeFor[PublishDataValue]() {
		return json.Unmarshal(raw, value.Addr().Interface())
	}
	if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		switch value.Kind() {
		case reflect.Pointer, reflect.Map, reflect.Slice:
			value.SetZero()
			return nil
		default:
			return fmt.Errorf("value must not be null")
		}
	}
	switch value.Kind() {
	case reflect.Pointer:
		value.Set(reflect.New(value.Type().Elem()))
		return decodeRequestValue(raw, value.Elem())
	case reflect.Struct:
		var fields map[string]json.RawMessage
		if err := json.Unmarshal(raw, &fields); err != nil {
			return err
		}
		return decodeRequestFields(value, fields)
	case reflect.Slice:
		var entries []json.RawMessage
		if err := json.Unmarshal(raw, &entries); err != nil {
			return err
		}
		value.Set(reflect.MakeSlice(value.Type(), len(entries), len(entries)))

View on GitHub (pinned to 9f775e8a12)