{"record":{"id":"c67a0f1642a2333a","repo":"siyuan-note/siyuan","slug":"field-s-must-not-be-null","errorCode":null,"errorMessage":"Field [%s] must not be null","messagePattern":"Field \\[(.+?)\\] must not be null","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/av_decode.go","lineNumber":25,"sourceCode":"\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// avDecodeBoundFields 仅对原本经过 JSON 结构体绑定的字段保留大小写匹配和数字归一化。\nfunc avDecodeBoundFields[Request any](reader io.Reader, path string, boundFields map[string]string, ignoredBools []string) (request Request, err error) {\n\tfields, err := blockRequestFields(reader, path)\n\tif err != nil {\n\t\treturn request, err\n\t}\n\toriginal := map[string]json.RawMessage{}\n\tfor name, placeholder := range boundFields {\n\t\traw, present := fields[name]\n\t\tif !present {\n\t\t\treturn request, fmt.Errorf(\"Field [%s] is required\", name)\n\t\t}\n\t\tif bytes.Equal(bytes.TrimSpace(raw), []byte(\"null\")) {\n\t\t\treturn request, fmt.Errorf(\"Field [%s] must not be null\", name)\n\t\t}\n\t\toriginal[name] = raw\n\t\tfields[name] = json.RawMessage(placeholder)\n\t}\n\tfor _, name := range ignoredBools {\n\t\toriginal[name] = fields[name]\n\t\tdelete(fields, name)\n\t}\n\tvalue := reflect.ValueOf(&request).Elem()\n\tif err = decodeRequestFields(value, fields); err != nil {\n\t\treturn request, err\n\t}\n\tfor i := 0; i < value.NumField(); i++ {\n\t\tfield := value.Type().Field(i)\n\t\tname := field.Tag.Get(\"json\")\n\t\traw, present := original[name]\n\t\tif !present {\n\t\t\tcontinue","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/av_decode.go#L7-L43","documentation":"In avDecodeBoundFields, a bound field present in the request but explicitly JSON null is rejected with 'Field [<name>] must not be null'. Bound fields carry structural payloads (e.g. filters data, attr values) that the decoder must normalize, and null would break the placeholder-based decoding.","triggerScenarios":"Sending JSON null for a bound field: {\"avID\":\"...\",\"data\":null} to /api/av/setAttrViewFilters or setAttrViewSorts, {\"value\":null} to setAttributeViewBlockAttr, {\"values\":null} to batchSetAttributeViewBlockAttrs, {\"blocksValues\":null}, {\"group\":null}.","commonSituations":"Clients initializing fields to null as a default, JSON serializers emitting null for undefined values, migrations from older API versions where null was tolerated.","solutions":["Send an empty container instead of null: [] for array fields (data/values/blocksValues), {} for object fields (value/group)","Coerce null to the empty default in the client before the request","Omit nothing else — the field must exist with a real value"],"exampleFix":"// before\nfetchPost(\"/api/av/setAttrViewSorts\", {avID, data: null});\n// after\nfetchPost(\"/api/av/setAttrViewSorts\", {avID, data: sorts ?? []});","handlingStrategy":"validation","validationCode":"function deNull(obj) { for (const k of [\"data\",\"value\",\"values\",\"blocksValues\",\"group\"]) if (obj[k] === null) obj[k] = Array.isArray(obj[k]) || [\"data\",\"values\",\"blocksValues\"].includes(k) ? [] : {}; return obj; }","typeGuard":"const isNotNull = (v) => v !== null && v !== undefined;","tryCatchPattern":"try { await post(avEndpoint, payload); } catch (e) { if (/must not be null/.test(e.message)) { payload = withDefaults(payload); /* retry once */ } else throw e; }","preventionTips":["Never serialize undefined attributes of request objects; set explicit defaults","Configure JSON serializers to omit null or map null to empty containers","Coerce null to [] / {} at the payload-construction site"],"tags":["validation","api","null-value"],"backgroundTag":"null-argument","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"}