{"record":{"id":"76bd2368d7f57321","repo":"OpenNHP/opennhp","slug":"data-must-be-a-struct","errorCode":null,"errorMessage":"data must be a struct","messagePattern":"data must be a struct","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nhp/core/ztdo/ztdo.go","lineNumber":491,"sourceCode":"\t\t}\n\n\t\tif !ztdo.signature.verify(recalcSig) {\n\t\t\treturn fmt.Errorf(\"signature verification failed\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// marshal searilizs Go struct into bytes buffer\nfunc marshal(buf *bytes.Buffer, data any) error {\n\trData := reflect.ValueOf(data)\n\tif rData.Kind() == reflect.Pointer {\n\t\trData = rData.Elem()\n\t}\n\n\tif rData.Kind() != reflect.Struct {\n\t\treturn fmt.Errorf(\"data must be a struct\")\n\t}\n\n\tfor i := range rData.NumField() {\n\t\tfield := rData.Field(i)\n\t\tif field.Type().Kind() == reflect.Struct {\n\t\t\tif err := marshal(buf, field.Interface()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tvar bytes []byte\n\t\t\tif field.Type().Kind() == reflect.Array { // here assume it's an array of byte.\n\t\t\t\tbytes = make([]byte, field.Len())\n\t\t\t\treflect.Copy(reflect.ValueOf(bytes), field)\n\t\t\t} else if field.Type().Kind() == reflect.Slice {\n\t\t\t\tif field.Type().Elem().Kind() == reflect.Struct {\n\t\t\t\t\tfor j := range field.Len() {\n\t\t\t\t\t\tif err := marshal(buf, field.Index(j).Interface()); err != nil {\n\t\t\t\t\t\t\treturn err","sourceCodeStart":473,"sourceCodeEnd":509,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/nhp/core/ztdo/ztdo.go#L473-L509","documentation":"The ztdo binary serializer uses reflection to marshal structs into bytes. marshal first dereferences one pointer level and then requires the resulting value to be a struct; anything else (map, slice, string, nil pointer after dereference, etc.) cannot be encoded by this format-specific serializer, so it returns this error.","triggerScenarios":"Passing a non-struct (or a nil pointer) to marshal directly, or via toBuffer; a nested struct field whose value is not a struct when marshal recurses into it; passing a typed nil pointer to a marshaling API.","commonSituations":"Calling toBuffer/marshal with a map[string]any assembled for JSON-style APIs; a variable typed any holding a non-struct at runtime; passing a pointer that is nil so Elem() yields an invalid (non-struct) value.","solutions":["Pass a struct (or pointer to struct) whose exported fields follow the ztdo encoding rules.","Check for nil before passing pointers: nil pointers dereference to invalid values.","Wrap non-struct payloads inside a dedicated struct field instead of marshaling them directly.","For dynamic payloads, first validate reflect.ValueOf(v).Kind() (after Elem()) == reflect.Struct."],"exampleFix":"// before\nbuf, err := toBuffer(payloadMap) // map -> \"data must be a struct\"\n// after\ntype Payload struct {\n    Data []byte\n}\nbuf, err := toBuffer(Payload{Data: raw})","handlingStrategy":"type-guard","validationCode":"func isMarshalable(v any) bool {\n    rv := reflect.ValueOf(v)\n    if rv.Kind() == reflect.Pointer {\n        if rv.IsNil() { return false }\n        rv = rv.Elem()\n    }\n    return rv.Kind() == reflect.Struct\n}","typeGuard":"if !isMarshalable(data) { return errors.New(\"toBuffer requires a non-nil struct or *struct\") }","tryCatchPattern":"buf, err := toBuffer(data)\nif err != nil {\n    if strings.Contains(err.Error(), \"data must be a struct\") {\n        return fmt.Errorf(\"%T is not ztdo-serializable: wrap it in a struct\", data)\n    }\n    return err\n}","preventionTips":["Only pass structs (or &struct) to marshal/toBuffer","Nil-check pointers before passing","Keep maps and non-byte slices out of wire structs"],"tags":["reflection","serialization","go","ztdo"],"backgroundTag":"type-mismatch","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}