{"record":{"id":"2d6946ed1ceeeb77","repo":"siyuan-note/siyuan","slug":"request-contract-must-be-a-struct","errorCode":null,"errorMessage":"request contract must be a struct","messagePattern":"request contract must be a struct","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/decode.go","lineNumber":44,"sourceCode":"\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"Parses request [%s] failed: %s\", e.definition.Path, err)\n\t\t}\n\t\treturn\n\t}\n\tvar fields map[string]json.RawMessage\n\terr = json.NewDecoder(reader).Decode(&fields)\n\tif err != nil {\n\t\tif e.definition.Body == LegacyOptionalBody {\n\t\t\treturn request, nil\n\t\t}\n\t\tif errors.Is(err, io.EOF) {\n\t\t\terr = errors.New(\"the request body is empty or truncated (EOF)\")\n\t\t}\n\t\treturn request, fmt.Errorf(\"Parses request [%s] failed: %s\", e.definition.Path, err)\n\t}\n\tvalue := reflect.ValueOf(&request).Elem()\n\tif value.Kind() != reflect.Struct {\n\t\treturn request, fmt.Errorf(\"request contract must be a struct\")\n\t}\n\terr = decodeRequestFields(value, fields)\n\treturn request, err\n}\n\nfunc decodeRequestFields(value reflect.Value, fields map[string]json.RawMessage) error {\n\tfor i := 0; i < value.NumField(); i++ {\n\t\tfield := value.Type().Field(i)\n\t\tname := strings.Split(field.Tag.Get(\"json\"), \",\")[0]\n\t\tif (!field.IsExported() && !field.Anonymous) || name == \"-\" {\n\t\t\tcontinue\n\t\t}\n\t\tif field.Anonymous && name == \"\" {\n\t\t\tif field.Type.Kind() != reflect.Struct {\n\t\t\t\treturn fmt.Errorf(\"unsupported embedded request field: %s\", field.Name)\n\t\t\t}\n\t\t\tif err := decodeRequestFields(value.Field(i), fields); err != nil {\n\t\t\t\treturn err","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/decode.go#L26-L62","documentation":"Decode asserts via reflection that the generic request type parameter is a struct after successful body decoding. If a contract is declared with a non-struct request type (pointer, map, slice, primitive), this error is returned. It is a contract-definition bug, not a client mistake.","triggerScenarios":"Defining an apicontract whose generic request parameter is e.g. map[string]string, a pointer type, or a scalar instead of a plain struct, then routing a request through it.","commonSituations":"Refactoring contracts and accidentally changing the type parameter; using *MyRequest instead of MyRequest; declaring shared generic contracts with the wrong instantiation.","solutions":["Change the contract's request type parameter to a plain struct type","Remove any pointer indirection (use MyRequest, not *MyRequest)","Add a compile-time/test-time check that every contract request type is a struct"],"exampleFix":"// before\ncontract.Define[map[string]any, Resp](path, handler)\n// after\ntype Req struct { ID string `json:\"id\"` }\ncontract.Define[Req, Resp](path, handler)","handlingStrategy":"validation","validationCode":"func assertStructContract[T any]() { var zero T; if reflect.TypeOf(zero) != nil && reflect.TypeOf(zero).Kind() != reflect.Struct { panic(\"contract request type must be a struct\") } }","typeGuard":"func isStruct[T any]() bool { t := reflect.TypeOf((*T)(nil)).Elem(); return t.Kind() == reflect.Struct }","tryCatchPattern":"if err := contract.Decode(...); err != nil { if strings.Contains(err.Error(), \"must be a struct\") { return fmt.Errorf(\"contract definition bug for %s: %w\", path, err) }; return err }","preventionTips":["Declare request types as plain structs, never maps/pointers/scalars","Add a contract-registry test asserting every request type is a struct","Review generic instantiations during contract refactors"],"tags":["reflection","api-contract","developer-error"],"backgroundTag":"internal-invariant-violation","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"}