gastownhall/beads · error

legacy SQLite issue %s 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.

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)

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.