gastownhall/beads · error

edge metadata is not well-formed JSON

Error message

edge metadata is not well-formed JSON

What it means

normalizeApplyEdgeMetadata validates the metadata string attached to a dependency edge. For non waits-for dependency types, if metadata is non-empty it must be well-formed JSON; this error means json.Valid failed on the trimmed string.

Source

Thrown at internal/storage/batch_apply.go:370

// A WAITS-FOR ROW MUST BE SELF-DESCRIBING. Readers that predate the gate's
// introduction do not default a missing one, so an absent, blank or `{}`
// metadata is written as {"gate":"all-children"} — the rule
// types.NewGraphEdgeDependency already applies on every other path that writes
// one, reached here so this role cannot drift from it. A metadata that names a
// gate keeps it, along with the spawner and also-blocks members a caller may
// carry; a gate that is neither known value is refused.
//
// Every other edge type's metadata is passed through unchanged, checked only
// for being well-formed JSON when it is present at all: the blob is
// type-specific and this role does not know the types.
func normalizeApplyEdgeMetadata(depType types.DependencyType, metadata string) (string, error) {
	trimmed := strings.TrimSpace(metadata)
	if depType != types.DepWaitsFor {
		if trimmed == "" {
			return "", nil
		}
		if !json.Valid([]byte(trimmed)) {
			return "", fmt.Errorf("edge metadata is not well-formed JSON")
		}
		return metadata, nil
	}
	meta := types.WaitsForMeta{}
	if trimmed != "" && trimmed != "{}" {
		if err := json.Unmarshal([]byte(trimmed), &meta); err != nil {
			return "", fmt.Errorf("waits-for metadata is not a well-formed gate object: %w", err)
		}
	}
	if meta.Gate == "" {
		meta.Gate = types.WaitsForAllChildren
	}
	if !types.IsValidWaitsForGate(meta.Gate) {
		return "", fmt.Errorf("waits-for gate %q is neither %q nor %q",
			meta.Gate, types.WaitsForAllChildren, types.WaitsForAnyChildren)
	}
	raw, err := json.Marshal(meta)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run the metadata through json.Marshal or a JSON linter before passing it
  2. Fix quoting when passing JSON via shell (single quotes inside, or heredoc)
  3. Pass empty string if no metadata is needed — empty is allowed

Example fix

// before
meta := "{reason: blocked by infra}"
// after
meta := `{"reason":"blocked by infra"}`
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(meta) != "" && !json.Valid([]byte(meta)) {
	return fmt.Errorf("metadata is not valid JSON: %q", meta)
}

Try / catch

var e *fmt.WrapError
if err != nil && strings.Contains(err.Error(), "edge metadata is not well-formed JSON") {
	// fall back to empty metadata or fix and re-submit
}

Prevention

When it happens

Trigger: Calling a batch dep-add with DepType other than waits-for and a metadata string that is not parseable JSON (e.g. "open", "{key: 1}", trailing comma, or plain text).

Common situations: Hand-written metadata in a CLI flag or YAML config; single-quoted JSON copied from shell; concatenating strings into metadata; template output that never got JSON-encoded.

Related errors


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