charmbracelet/crush · error

failed to marshal provider data

Error message

failed to marshal provider data: %w

What it means

Thrown by cache.Store() when json.Marshal fails to serialize the provider data being cached. Rare in practice because the value is a well-typed struct, but can occur with values containing unsupported types (channels, funcs, cyclic references) when the generic cache[T] is used with a non-marshalable T.

Solutions

  1. Inspect the value being stored for unsupported JSON types (chan, func, cycles).
  2. Fix the producer of the data to only include JSON-serializable fields.
  3. Add json:"-" tags to non-serializable fields in the type parameter T.
  4. Wrap Marshal yourself with a custom codec if you need special serialization.

Example fix

// before
type P struct { Conn *net.Conn }
// after
type P struct { Conn *net.Conn `json:"-"` }
Defensive patterns

Strategy: validation

Validate before calling

if err := json.Marshal(v); err != nil {
    // value not serializable; fix before Store
}

Type guard

func marshalable[T any](v T) bool {
    _, err := json.Marshal(v)
    return err == nil
}

Try / catch

if err := cache.Store(v); err != nil {
    if strings.Contains(err.Error(), "marshal") {
        log.Warn("Skipping cache: value not serializable")
    }
}

Prevention

When it happens

Trigger: cache.Store(v) is called with a T that json.Marshal cannot encode: unsupported field types, cycles, or invalid values (NaN) injected into the provider data by a caller.

Common situations: Custom builds/extensions feeding non-serializable data into the generic cache; middlewares mutating provider structs with extra fields of unsupported types.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/3f685d1d26d3de25. Report an issue: GitHub.

Appendix: source

Thrown at internal/config/provider.go:292

		return v, "", fmt.Errorf("failed to read provider cache file: %w", err)
	}

	if err := json.Unmarshal(data, &v); err != nil {
		return v, "", fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
	}

	return v, etag.Of(data), nil
}

func (c cache[T]) Store(v T) error {
	slog.Info("Saving provider data to disk", "path", c.path)
	if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil {
		return fmt.Errorf("failed to create directory for provider cache: %w", err)
	}

	data, err := json.Marshal(v)
	if err != nil {
		return fmt.Errorf("failed to marshal provider data: %w", err)
	}

	// Written through a temporary file and renamed into place. Several Crush
	// instances start independently and race to refresh this cache, and a
	// truncating write would let one of them read a half-written catalog and
	// silently fall back to the bundled copy.
	if err := atomicWriteFile(c.path, data, 0o644); err != nil {
		return fmt.Errorf("failed to write provider data to cache: %w", err)
	}
	return nil
}

View on GitHub (pinned to 7944b8e522)