{"record":{"id":"0cab70bc0f0ee897","repo":"siyuan-note/siyuan","slug":"block-operation-context-s-must-be-text","errorCode":null,"errorMessage":"block operation context [%s] must be text","messagePattern":"block operation context \\[(.+?)\\] must be text","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/contract_block_transaction.go","lineNumber":73,"sourceCode":"\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = json.Unmarshal(data, &ret.RetData); err != nil {\n\t\treturn nil, err\n\t}\n\tdata, err = json.Marshal(operation.Context)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar contextFields map[string]*string\n\tif err = json.Unmarshal(data, &contextFields); err != nil {\n\t\treturn nil, err\n\t}\n\tif contextFields != nil {\n\t\tret.Context = make(map[string]string, len(contextFields))\n\t\tfor key, value := range contextFields {\n\t\t\tif value == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"block operation context [%s] must be text\", key)\n\t\t\t}\n\t\t\tret.Context[key] = *value\n\t\t}\n\t}\n\treturn ret, nil\n}\n\nfunc blockOperationsContract(operations []*model.Operation) ([]*apicontract.BlockOperation, error) {\n\tif operations == nil {\n\t\treturn nil, nil\n\t}\n\tret := make([]*apicontract.BlockOperation, len(operations))\n\tfor i, operation := range operations {\n\t\tconverted, err := blockOperationContract(operation)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tret[i] = converted","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/api/contract_block_transaction.go#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nctx := map[string]*string{\"note\": nil, \"ref\": strPtr(\"2024\")}\nop := &model.Operation{Action: \"insert\", Context: ctx}\n// after\nfor k, v := range ctx {\n  if v == nil {\n    delete(ctx, k) // or substitute strPtr(\"\")\n  }\n}\nop := &model.Operation{Action: \"insert\", Context: ctx}","handlingStrategy":"validation","validationCode":"const sanitizeContext = (ctx) => ctx && Object.fromEntries(Object.entries(ctx).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));","typeGuard":"const hasTextOnlyContext = (op) => !op.Context || Object.values(op.Context).every(v => typeof v === 'string');","tryCatchPattern":"try {\n  return convertOperationContract(op);\n} catch (e) {\n  if (String(e?.message ?? e?.msg).includes('must be text')) {\n    op.Context = sanitizeContext(rawContext);\n    return convertOperationContract(op);\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["transactions","validation","contract","siyuan"],"backgroundTag":"schema-validation-failed","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}