gastownhall/beads · error

must be an array of strings

Error message

must be an array of strings

What it means

decodeWaiters parses the legacy waiters column as JSON and requires the top-level value to be a JSON array. If json.Unmarshal succeeds but the value is not an array (object, string, number, etc.), the migration returns 'must be an array of strings'.

Solutions

  1. Convert the legacy waiters value to a JSON array of strings before migration.
  2. Identify the legacy format and write a converter to the expected array form.
  3. Correct the malformed row directly in the legacy database.

Example fix

// before (legacy value)
{"waiters": ["alice"]}
// after
["alice"]
Defensive patterns

Strategy: type-guard

Validate before calling

var v any; if json.Unmarshal([]byte(raw), &v) != nil || _, ok := v.([]any); !ok { return errors.New("waiters must be a JSON array") }

Type guard

func isStringArray(v any) bool { _, ok := v.([]any); return ok }

Try / catch

waiters, err := decodeWaiters(raw); if err != nil { log.Warn("unparseable waiters, defaulting to empty", err); waiters = nil }

Prevention

When it happens

Trigger: decodeWaiters receives a raw string that unmarshals to a non-[]any value, e.g. '{"a":1}' or '"foo"'.

Common situations: Waiters stored in an older format (CSV, object map) in the legacy DB, hand-edited rows, or a schema change between legacy versions.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/5c4097678b1fee46. Report an issue: GitHub.

Appendix: source

Thrown at internal/migration/legacysqlite/reader.go:695

func validateCurrentTextBytes(issue *types.Issue) error {
	if len(issue.Payload) > currentTextBytes {
		return fmt.Errorf("legacy SQLite issue payload is %d bytes (current TEXT maximum %d)", len(issue.Payload), currentTextBytes)
	}
	waiters := issueops.FormatJSONStringArray(issue.Waiters)
	if len(waiters) > currentTextBytes {
		return fmt.Errorf("legacy SQLite issue waiters serialize to %d bytes (current TEXT maximum %d)", len(waiters), currentTextBytes)
	}
	return nil
}

func decodeWaiters(raw string) ([]string, error) {
	var decoded any
	if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
		return nil, err
	}
	values, ok := decoded.([]any)
	if !ok {
		return nil, fmt.Errorf("must be an array of strings")
	}
	waiters := make([]string, len(values))
	for i, value := range values {
		waiter, ok := value.(string)
		if !ok {
			return nil, fmt.Errorf("element %d is not a string", i)
		}
		if err := checkUTF8(currentString{fmt.Sprintf("waiters[%d]", i), waiter}); err != nil {
			return nil, err
		}
		waiters[i] = waiter
	}
	return waiters, nil
}

func validateIssueVarchars(issue *types.Issue) error {
	fields := []currentVarchar{
		{"issue id", issue.ID, types.MaxFieldLen},

View on GitHub (pinned to 71377f2769)