siyuan-note/siyuan · error

block operation result must be text, block IDs or null

Error message

block operation result must be text, block IDs or null

What it means

UnmarshalJSON for the block operation result value accepts exactly three shapes: a JSON string (text), a JSON array of block IDs, or JSON null. Any other top-level shape (object, number, bool, empty input) produces this error. It keeps the discriminated-union result type strict so downstream code can safely switch on the payload kind.

Solutions

  1. Send a JSON string if the operation result is text
  2. Send a JSON array of block-ID strings if the operation produced blocks
  3. Send literal null when there is no payload
  4. Inspect the actual response body — an object usually means an error response was passed to the wrong decoder

Example fix

// before
{"result": {"ids": ["20240101120000-abc"]}}
// after
{"result": ["20240101120000-abc"]}
Defensive patterns

Strategy: type-guard

Validate before calling

function isValidResult(v) { return v === null || typeof v === "string" || (Array.isArray(v) && v.every(x => typeof x === "string")); }

Type guard

function isBlockOpResult(v) { return v === null || typeof v === "string" || (Array.isArray(v) && v.every(x => typeof x === "string")); }

Try / catch

try { result = decodeOpResult(raw); } catch (e) { if (String(e).includes("must be text, block IDs or null")) { result = null; log.warn("unexpected op result shape", raw); } else throw e; }

Prevention

When it happens

Trigger: Decoding a block operation result that is a JSON object, number, boolean, or empty byte slice; the check `data[0] != '['` after the string fast-path fails for anything that is neither a quoted string nor an array.

Common situations: A server or proxy returning an error object {"error": ...} where a result value is expected; clients misusing the field with a numeric count instead of the documented text/IDs/null; truncated or empty response bodies.

Related errors


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

Appendix: source

Thrown at kernel/apicontract/block_transaction.go:86

func (r BlockOperationResult) MarshalJSON() ([]byte, error) {
	if r.text != nil {
		return json.Marshal(*r.text)
	}
	return json.Marshal(r.ids)
}

func (r *BlockOperationResult) UnmarshalJSON(data []byte) error {
	data = bytes.TrimSpace(data)
	*r = BlockOperationResult{}
	if bytes.Equal(data, []byte("null")) {
		return nil
	}
	if len(data) > 0 && data[0] == '"' {
		return json.Unmarshal(data, &r.text)
	}
	var values []json.RawMessage
	if len(data) == 0 || data[0] != '[' {
		return fmt.Errorf("block operation result must be text, block IDs or null")
	}
	if err := json.Unmarshal(data, &values); err != nil {
		return err
	}
	r.ids = make([]string, len(values))
	for i, value := range values {
		if bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
			return fmt.Errorf("block ID must be text")
		}
		if err := json.Unmarshal(value, &r.ids[i]); err != nil {
			return err
		}
	}
	return nil
}

type BlockOperation struct {
	Action            string               `json:"action" api:"enum=delete|insert|update|foldHeading|unfoldHeading|setAttrs|moveOutlineHeading|appendInsert|prependInsert"`

View on GitHub (pinned to 9f775e8a12)