{"record":{"id":"74c5ad703b17f3ba","repo":"siyuan-note/siyuan","slug":"options-must-not-be-empty-use-optionremove-to-delete","errorCode":null,"errorMessage":"options must not be empty; use optionRemove to delete existing options","messagePattern":"options must not be empty; use optionRemove to delete existing options","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view_key_config.go","lineNumber":200,"sourceCode":"\t\treturn errors.New(\"configuration must not be null\")\n\t}\n\tdata, err := json.Marshal(value)\n\tif nil != err {\n\t\treturn err\n\t}\n\tdecoder := json.NewDecoder(bytes.NewReader(data))\n\tdecoder.DisallowUnknownFields()\n\treturn decoder.Decode(dest)\n}\n\nfunc updateAttributeViewKeyOptions(attrView *av.AttributeView, key *av.Key, op *Operation, setting string, value any) error {\n\tif \"options\" == setting {\n\t\tvar options []*av.SelectOption\n\t\tif err := decodeAttributeViewKeyConfig(value, &options); nil != err {\n\t\t\treturn err\n\t\t}\n\t\tif 0 == len(options) {\n\t\t\treturn errors.New(\"options must not be empty; use optionRemove to delete existing options\")\n\t\t}\n\t\tnames := map[string]bool{}\n\t\tfor _, option := range options {\n\t\t\tif nil == option || \"\" == strings.TrimSpace(option.Name) {\n\t\t\t\treturn errors.New(\"each option requires a nonempty name\")\n\t\t\t}\n\t\t\tif names[option.Name] {\n\t\t\t\treturn errors.New(\"duplicate option name\")\n\t\t\t}\n\t\t\tif attrView.FilterColorValue(option.Color) != option.Color {\n\t\t\t\treturn errors.New(\"invalid option color\")\n\t\t\t}\n\t\t\tnames[option.Name] = true\n\t\t}\n\t\top.Data = options\n\t\treturn updateAttributeViewColumnOptions(op)\n\t}\n\tif \"optionRemove\" == setting {","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view_key_config.go#L182-L218","documentation":"When replacing the option list of a select/mselect field ('options' setting), the new list must contain at least one option. SiYuan deliberately forbids clearing all options through this path and directs you to the 'optionRemove' setting, which safely removes a single existing option and handles rows that reference it.","triggerScenarios":"Calling UpdateAttributeViewKeyConfig with setting \"options\" and an empty array (or null-length decode result) for a select/multi-select key.","commonSituations":"Frontend sends the current option list minus the one the user deleted, leaving zero items; bulk-import code writes an empty array for fields with no options yet; template that resets a field's options to [].","solutions":["Send the full desired non-empty option list instead of an empty array","Use setting \"optionRemove\" with the option's name to delete a single existing option","If the field truly must have no options, delete the field and recreate it, or add at least one placeholder option"],"exampleFix":"// before\n{avID, keyID, setting: \"options\", data: []}\n// after\n{avID, keyID, setting: \"optionRemove\", data: \"Unused Option\"}","handlingStrategy":"validation","validationCode":"if (setting === \"options\" && (!Array.isArray(options) || options.length === 0)) {\n  throw new Error(\"use optionRemove to delete an option, cannot send empty options list\");\n}","typeGuard":"const canReplaceOptions = (opts) => Array.isArray(opts) && opts.length > 0;","tryCatchPattern":null,"preventionTips":["Treat 'options' as full-list replacement only when at least one option remains","Route single deletions through the optionRemove setting"],"tags":["attribute-view","select-options","validation"],"backgroundTag":"empty-required-field","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"}