gastownhall/beads · error · storage.ErrValidation

%w: metadata value for key %q is not valid JSON

Error message

%w: metadata value for key %q is not valid JSON

What it means

ApplyMetadataPatch wraps storage.ErrValidation when a value in patch.Set for a given key is not valid JSON. Metadata values are stored as raw JSON, so every set value must parse; the offending key is named in the message.

Source

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

				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" {
				if err := json.Unmarshal(next, &values); err != nil {
					return nil, false, fmt.Errorf("%w: metadata edits require an object: %v", storage.ErrValidation, err)
				}
			}
			for _, key := range setKeys {
				value := patch.Set[key]
				if !json.Valid(value) {
					return nil, false, fmt.Errorf("%w: metadata value for key %q is not valid JSON", storage.ErrValidation, key)
				}
				values[key] = append(json.RawMessage(nil), value...)
			}
			for _, key := range patch.Unset {
				delete(values, key)
			}
			encoded, err := json.Marshal(values)
			if err != nil {
				return nil, false, fmt.Errorf("%w: encode metadata edits: %v", storage.ErrValidation, err)
			}
			next = encoded
		}
	}
	if err := ValidateMetadataIfConfigured(next); err != nil {
		return nil, false, err
	}
	changed, err := metadataChanged(current, next)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Marshal values with encoding/json (json.Marshal / json.Marshaler) instead of casting raw strings to json.RawMessage.
  2. Pre-validate each value with json.Valid in the caller before building the patch.
  3. For string values, use json.Marshal(string) to get proper quoting.

Example fix

// before
patch.Set["name"] = json.RawMessage(`Alice`) // invalid JSON
// after
b, _ := json.Marshal("Alice")
patch.Set["name"] = b // "Alice" (quoted, valid JSON)
Defensive patterns

Strategy: validation

Validate before calling

func validSetValues(set map[string]json.RawMessage) error {
    for k, v := range set {
        if !json.Valid(v) {
            return fmt.Errorf("key %q: invalid JSON value", k)
        }
    }
    return nil
}

Try / catch

if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "not valid JSON") { /* fix the named key's value, no retry */ }

Prevention

When it happens

Trigger: Calling ApplyMetadataPatch with patch.Set[key] set to non-JSON bytes, e.g. plain strings without quotes ("hello" instead of "\"hello\""), trailing commas, or partially marshaled values.

Common situations: Passing CLI flag values directly as raw JSON without quoting strings; hand-concatenating JSON; converting user text to json.RawMessage instead of json.Marshal; truncated values from config pipelines.

Related errors


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