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

  1. Pass a struct (or pointer to struct) whose exported fields follow the ztdo encoding rules.
  2. Check for nil before passing pointers: nil pointers dereference to invalid values.
  3. Wrap non-struct payloads inside a dedicated struct field instead of marshaling them directly.
  4. 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

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


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 err

View on GitHub (pinned to 6e04ca5ff0)