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'.

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)

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.