OpenNHP/opennhp · error
data must be a struct
Error message
data must be a struct
What it means
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.
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.
Example fix
// before
buf, err := toBuffer(payloadMap) // map -> "data must be a struct"
// after
type Payload struct {
Data []byte
}
buf, err := toBuffer(Payload{Data: raw}) Defensive patterns
Strategy: type-guard
Validate before calling
func isMarshalable(v any) bool {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
if rv.IsNil() { return false }
rv = rv.Elem()
}
return rv.Kind() == reflect.Struct
} Type guard
if !isMarshalable(data) { return errors.New("toBuffer requires a non-nil struct or *struct") } Try / catch
buf, err := toBuffer(data)
if err != nil {
if strings.Contains(err.Error(), "data must be a struct") {
return fmt.Errorf("%T is not ztdo-serializable: wrap it in a struct", data)
}
return err
} Prevention
- Only pass structs (or &struct) to marshal/toBuffer
- Nil-check pointers before passing
- Keep maps and non-byte slices out of wire structs
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- unsupported field type
- unsupported field type
- failed to marshal evidence
- nhp server length is too long
- unsupported element type
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/76bd2368d7f57321.
Report an issue: GitHub.
Appendix: source
Thrown at nhp/core/ztdo/ztdo.go:491
}
if !ztdo.signature.verify(recalcSig) {
return fmt.Errorf("signature verification failed")
}
}
return nil
}
// marshal searilizs Go struct into bytes buffer
func marshal(buf *bytes.Buffer, data any) error {
rData := reflect.ValueOf(data)
if rData.Kind() == reflect.Pointer {
rData = rData.Elem()
}
if rData.Kind() != reflect.Struct {
return fmt.Errorf("data must be a struct")
}
for i := range rData.NumField() {
field := rData.Field(i)
if field.Type().Kind() == reflect.Struct {
if err := marshal(buf, field.Interface()); err != nil {
return err
}
} else {
var bytes []byte
if field.Type().Kind() == reflect.Array { // here assume it's an array of byte.
bytes = make([]byte, field.Len())
reflect.Copy(reflect.ValueOf(bytes), field)
} else if field.Type().Kind() == reflect.Slice {
if field.Type().Elem().Kind() == reflect.Struct {
for j := range field.Len() {
if err := marshal(buf, field.Index(j).Interface()); err != nil {
return errView on GitHub (pinned to 6e04ca5ff0)