gastownhall/beads · error · storage.ErrValidation

%w: metadata replacement is not valid JSON

Error message

%w: metadata replacement is not valid JSON

What it means

ApplyMetadataPatch throws this wrapped storage.ErrValidation when patch.Replace.Set is true but the replacement metadata document is not valid JSON. The library requires the entire metadata blob to always be parseable JSON, so it rejects the update before writing.

Source

Thrown at internal/storage/issueops/aggregate.go:215

	sort.Strings(setKeys)
	for _, key := range setKeys {
		if err := storage.ValidateMetadataKey(key); err != nil {
			return nil, false, fmt.Errorf("%w: %w", storage.ErrValidation, err)
		}
	}
	for _, key := range patch.Unset {
		if err := storage.ValidateMetadataKey(key); err != nil {
			return nil, false, fmt.Errorf("%w: %w", storage.ErrValidation, err)
		}
	}
	var next json.RawMessage
	if patch.Replace.Set {
		next = append(json.RawMessage(nil), patch.Replace.Value...)
		if len(next) == 0 {
			next = json.RawMessage(`{}`)
		}
		if !json.Valid(next) {
			return nil, false, fmt.Errorf("%w: metadata replacement is not valid JSON", storage.ErrValidation)
		}
	} else {
		next = append(json.RawMessage(nil), current...)
		if patch.Merge.Set {
			// A JSON null unmarshals into a nil overlay map, so the merge
			// below would silently accept it as "change nothing".
			if strings.TrimSpace(string(patch.Merge.Value)) == "null" {
				return nil, false, fmt.Errorf("%w: metadata merge must be a JSON object", storage.ErrValidation)
			}
			merged, err := storage.MergeMetadataJSON(next, patch.Merge.Value)
			if err != nil {
				return nil, false, fmt.Errorf("%w: metadata merge: %v", storage.ErrValidation, err)
			}
			next = merged
		}
		if len(patch.Set) > 0 || len(patch.Unset) > 0 {
			values := make(map[string]json.RawMessage)
			if len(next) > 0 && string(next) != "null" {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run json.Valid on the replacement document in the caller before building the patch.
  2. Marshal a Go map/struct with encoding/json instead of hand-building JSON strings.
  3. Fix shell quoting or file content so the value is a complete, valid JSON object.

Example fix

// before
req.MetadataReplace = []byte(`{"a": 1`) // truncated
// after
v := map[string]any{"a": 1}
b, err := json.Marshal(v)
if err != nil {
    return err
}
req.MetadataReplace = b // guaranteed valid JSON
Defensive patterns

Strategy: validation

Validate before calling

func validReplace(v json.RawMessage) bool {
    t := strings.TrimSpace(string(v))
    return t == "" || json.Valid(v)
}

Try / catch

if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "not valid JSON") { /* fix replacement document, no retry */ }

Prevention

When it happens

Trigger: Calling ApplyMetadataPatch with patch.Replace.Set=true and patch.Replace.Value being invalid JSON (e.g. raw text, truncated JSON, or a value produced by naive string concatenation).

Common situations: Shell quoting mangling `--metadata '{"a":1'`; templates that interpolate into JSON strings; reading a corrupted metadata blob from a config file; passing an empty value (which the code silently coerces to `{}`) followed by non-JSON text.

Related errors


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