{"record":{"id":"d4091523534b1170","repo":"gastownhall/beads","slug":"edge-metadata-is-not-well-formed-json","errorCode":null,"errorMessage":"edge metadata is not well-formed JSON","messagePattern":"edge metadata is not well-formed JSON","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/batch_apply.go","lineNumber":370,"sourceCode":"// A WAITS-FOR ROW MUST BE SELF-DESCRIBING. Readers that predate the gate's\n// introduction do not default a missing one, so an absent, blank or `{}`\n// metadata is written as {\"gate\":\"all-children\"} — the rule\n// types.NewGraphEdgeDependency already applies on every other path that writes\n// one, reached here so this role cannot drift from it. A metadata that names a\n// gate keeps it, along with the spawner and also-blocks members a caller may\n// carry; a gate that is neither known value is refused.\n//\n// Every other edge type's metadata is passed through unchanged, checked only\n// for being well-formed JSON when it is present at all: the blob is\n// type-specific and this role does not know the types.\nfunc normalizeApplyEdgeMetadata(depType types.DependencyType, metadata string) (string, error) {\n\ttrimmed := strings.TrimSpace(metadata)\n\tif depType != types.DepWaitsFor {\n\t\tif trimmed == \"\" {\n\t\t\treturn \"\", nil\n\t\t}\n\t\tif !json.Valid([]byte(trimmed)) {\n\t\t\treturn \"\", fmt.Errorf(\"edge metadata is not well-formed JSON\")\n\t\t}\n\t\treturn metadata, nil\n\t}\n\tmeta := types.WaitsForMeta{}\n\tif trimmed != \"\" && trimmed != \"{}\" {\n\t\tif err := json.Unmarshal([]byte(trimmed), &meta); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"waits-for metadata is not a well-formed gate object: %w\", err)\n\t\t}\n\t}\n\tif meta.Gate == \"\" {\n\t\tmeta.Gate = types.WaitsForAllChildren\n\t}\n\tif !types.IsValidWaitsForGate(meta.Gate) {\n\t\treturn \"\", fmt.Errorf(\"waits-for gate %q is neither %q nor %q\",\n\t\t\tmeta.Gate, types.WaitsForAllChildren, types.WaitsForAnyChildren)\n\t}\n\traw, err := json.Marshal(meta)\n\tif err != nil {","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/batch_apply.go#L352-L388","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Run the metadata through json.Marshal or a JSON linter before passing it","Fix quoting when passing JSON via shell (single quotes inside, or heredoc)","Pass empty string if no metadata is needed — empty is allowed"],"exampleFix":"// before\nmeta := \"{reason: blocked by infra}\"\n// after\nmeta := `{\"reason\":\"blocked by infra\"}`","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(meta) != \"\" && !json.Valid([]byte(meta)) {\n\treturn fmt.Errorf(\"metadata is not valid JSON: %q\", meta)\n}","typeGuard":null,"tryCatchPattern":"var e *fmt.WrapError\nif err != nil && strings.Contains(err.Error(), \"edge metadata is not well-formed JSON\") {\n\t// fall back to empty metadata or fix and re-submit\n}","preventionTips":["Always build metadata with json.Marshal, never string concatenation","Mind shell quoting when passing JSON on the command line","Leave metadata empty when unused — empty is accepted"],"tags":["json","validation","edge-metadata"],"backgroundTag":"invalid-json-metadata","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}