siyuan-note/siyuan · error
block operation context
Error message
block operation context [%s] must be text
What it means
A block operation's optional Context map must contain only string values. During contract conversion, contextFields values are dereferenced as *string; a nil value means a non-text (null/typed) value sneaked into the context, so blockOperationContract fails with the offending key named in the message. This protects the public contract, which only allows text context entries.
Solutions
- Ensure every Context entry is a non-nil string before building the operation; drop keys with null values
- Coerce non-text values to strings client-side (String(value)) or remove them
- Sanitize deserialized context maps: iterate keys and delete entries whose value is nil before calling the API
- If a non-text value must be stored, encode it as a string (e.g. JSON-encode the value) instead of a typed value
Example fix
// before
ctx := map[string]*string{"note": nil, "ref": strPtr("2024")}
op := &model.Operation{Action: "insert", Context: ctx}
// after
for k, v := range ctx {
if v == nil {
delete(ctx, k) // or substitute strPtr("")
}
}
op := &model.Operation{Action: "insert", Context: ctx} Defensive patterns
Strategy: validation
Validate before calling
const sanitizeContext = (ctx) => ctx && Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
Type guard
const hasTextOnlyContext = (op) => !op.Context || Object.values(op.Context).every(v => typeof v === 'string');
Try / catch
try {
return convertOperationContract(op);
} catch (e) {
if (String(e?.message ?? e?.msg).includes('must be text')) {
op.Context = sanitizeContext(rawContext);
return convertOperationContract(op);
}
throw e;
} Prevention
- Always serialize context values to strings before building operations
- Strip null-valued keys from context maps after JSON deserialization
- Type Context as Record<string, string> end-to-end so typed values cannot enter
When it happens
Trigger: Constructing a model.Operation whose Context map contains a nil pointer or non-string-typed value and passing it through blockOperationContract — e.g. plugin/kernel code copying a generic map[string]any into Context, or deserialized JSON with null values for some keys.
Common situations: JSON payloads like {"context":{"note":null}} deserialized to nil *string; generic map conversion helpers assigning any-typed values; old clients sending numeric/boolean context values that no longer fit the text-only contract.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- unexpected attribute view payload in block operation
- 106
- 199
- Cannot create a file starting with .
- Conf.Language(0)
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/0cab70bc0f0ee897.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/api/contract_block_transaction.go:73
if err != nil {
return nil, err
}
if err = json.Unmarshal(data, &ret.RetData); err != nil {
return nil, err
}
data, err = json.Marshal(operation.Context)
if err != nil {
return nil, err
}
var contextFields map[string]*string
if err = json.Unmarshal(data, &contextFields); err != nil {
return nil, err
}
if contextFields != nil {
ret.Context = make(map[string]string, len(contextFields))
for key, value := range contextFields {
if value == nil {
return nil, fmt.Errorf("block operation context [%s] must be text", key)
}
ret.Context[key] = *value
}
}
return ret, nil
}
func blockOperationsContract(operations []*model.Operation) ([]*apicontract.BlockOperation, error) {
if operations == nil {
return nil, nil
}
ret := make([]*apicontract.BlockOperation, len(operations))
for i, operation := range operations {
converted, err := blockOperationContract(operation)
if err != nil {
return nil, err
}
ret[i] = convertedView on GitHub (pinned to 9f775e8a12)