gastownhall/beads · error

legacy SQLite issue has invalid metadata JSON

Error message

legacy SQLite issue %s has invalid metadata JSON

What it means

applyMetadataAndWaiters checks that a legacy issue's non-empty metadata column is well-formed JSON via json.Valid; if not, it reports 'legacy SQLite issue <id> has invalid metadata JSON'. Valid JSON is required because metadata is stored verbatim into the new issue record.

Solutions

  1. Inspect the metadata column for the reported issue ID and repair it into valid JSON (or set it to NULL/'{}'), then re-run migration.
  2. Re-serialize the intended key/value pairs into a valid JSON object before migrating.
  3. Clear the corrupted metadata (NULL) if it can be reconstructed after migration.

Example fix

-- before
UPDATE issues SET metadata='{"key": ' WHERE id='bd-3';
-- after
UPDATE issues SET metadata='{"key": "value"}' WHERE id='bd-3';
Defensive patterns

Strategy: validation

Validate before calling

rows, _ := db.Query(`SELECT id FROM issues WHERE metadata IS NOT NULL AND metadata != '' `)
for rows.Next() {
  var id, meta string
  rows.Scan(&id, &meta)
  if !json.Valid([]byte(meta)) { /* repair before migrate */ }
}

Try / catch

if !json.Valid([]byte(metadata)) {
  // re-serialize or NULL the metadata column before migrating
}

Prevention

When it happens

Trigger: Migrating a legacy DB where an issues.metadata column holds text that is not parseable JSON (truncated blob, plain string, concatenated fragments).

Common situations: Truncated TEXT columns from interrupted writes; manual edits appending prose to the JSON; encoding bugs in old exporter tools; NULL-terminator or BOM artifacts breaking the parser.

Related errors


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

Appendix: source

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

	}
	if issue.CompactedAt, err = parseOptionalTime("compacted_at", x.compactedAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s: %w", issue.ID, err)
	}
	if issue.DueAt, err = parseOptionalTime("due_at", x.dueAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s: %w", issue.ID, err)
	}
	if issue.DeferUntil, err = parseOptionalTime("defer_until", x.deferUntil); err != nil {
		return fmt.Errorf("legacy SQLite issue %s: %w", issue.ID, err)
	}
	return nil
}

// applyMetadataAndWaiters validates the legacy metadata and waiters JSON blobs
// (well-formed and free of unpaired surrogates) and assigns them to issue.
// metadata is stored verbatim unless it is the empty object; waiters is decoded.
func (x legacyExtras) applyMetadataAndWaiters(issue *types.Issue) error {
	if x.metadata.Valid && x.metadata.String != "" && !json.Valid([]byte(x.metadata.String)) {
		return fmt.Errorf("legacy SQLite issue %s has invalid metadata JSON", issue.ID)
	}
	if x.metadata.Valid && x.metadata.String != "" {
		if err := checkJSONSurrogates(x.metadata.String); err != nil {
			return fmt.Errorf("legacy SQLite issue %s metadata: %w", issue.ID, err)
		}
	}
	if x.metadata.Valid && x.metadata.String != "" && x.metadata.String != "{}" {
		issue.Metadata = []byte(x.metadata.String)
	}
	if x.waiters.Valid && x.waiters.String != "" {
		if !json.Valid([]byte(x.waiters.String)) {
			return fmt.Errorf("issue %s waiters: invalid JSON", issue.ID)
		}
		if err := checkJSONSurrogates(x.waiters.String); err != nil {
			return fmt.Errorf("issue %s waiters: %w", issue.ID, err)
		}
		waiters, err := decodeWaiters(x.waiters.String)
		if err != nil {

View on GitHub (pinned to 71377f2769)