hashicorp/terraform · error

Failed to marshal index step key %#v

Error message

Failed to marshal index step key %#v: %s

What it means

Thrown by jsonplan.encodePath when converting a cty.Path (used for sensitive-attribute paths and diff paths) to JSON. For a cty.IndexStep, the key is encoded with ctyjson.Marshal(s.Key, s.Key.Type()). If that marshal fails (e.g. the key is a null/unknown value, or has an exotic cty type ctyjson cannot encode), the error wraps the underlying failure. The plan JSON then cannot represent which indexed element is sensitive/changed.

Solutions

  1. Upgrade Terraform/ctyjson; newer versions handle more key types.
  2. Simplify the index key type (use strings/numbers rather than complex objects as for_each keys).
  3. Reduce sensitivity markers on dynamically-indexed attributes to isolate the offending path.
  4. Reproduce with `terraform plan -json` and report the key type (%#v) shown in the message upstream.

Example fix

// before
key, err := ctyjson.Marshal(s.Key, s.Key.Type())
if err != nil {
	return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err)
}

// after (handle null/unknown keys defensively before encoding)
if !s.Key.IsKnown() || s.Key.IsNull() {
	return nil, fmt.Errorf("cannot encode path with unknown/null index key: %#v", s.Key)
}
key, err := ctyjson.Marshal(s.Key, s.Key.Type())
Defensive patterns

Strategy: try-catch

Validate before calling

// Reject null/unknown index keys before they reach ctyjson.
func encodablePath(p cty.Path) error {
    for _, step := range p {
        if idx, ok := step.(cty.IndexStep); ok {
            if !idx.Key.IsKnown() || idx.Key.IsNull() {
                return fmt.Errorf("path contains unencodable index key: %#v", idx.Key)
            }
        }
    }
    return nil
}

Type guard

func isKnownIndexStep(s cty.PathStep) bool {
    idx, ok := s.(cty.IndexStep)
    if !ok {
        return true
    }
    return idx.Key.IsKnown() && !idx.Key.IsNull()
}

Try / catch

raw, err := jsonplan.MarshalChanges(changes)
if err != nil && strings.Contains(err.Error(), "Failed to marshal index step key") {
    // surface a clearer error pointing at the for_each/count config
    return fmt.Errorf("cannot JSON-encode plan: a sensitive/changed value sits at an index path with an unencodable key; simplify for_each keys: %w", err)
}

Prevention

When it happens

Trigger: A resource uses for_each/count and a sensitive value sits at an indexed path whose key is null, unknown, or of a type ctyjson rejects; a provider emits an attribute path with an unusual index key type.

Common situations: for_each over a map with sensitive nested attributes; plan output (-json) for a configuration combining dynamic blocks with sensitive markers; provider schema returning paths with object/tuple-typed index keys.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/02ac1c1de9dcf8e1. Report an issue: GitHub.

Appendix: source

Thrown at internal/command/jsonplan/plan.go:1072

	for _, path := range pathList {
		jsonPath, err := encodePath(path)
		if err != nil {
			return nil, err
		}
		jsonPaths = append(jsonPaths, jsonPath)
	}

	return json.Marshal(jsonPaths)
}

func encodePath(path cty.Path) (json.RawMessage, error) {
	steps := make([]json.RawMessage, 0, len(path))
	for _, step := range path {
		switch s := step.(type) {
		case cty.IndexStep:
			key, err := ctyjson.Marshal(s.Key, s.Key.Type())
			if err != nil {
				return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err)
			}
			steps = append(steps, key)
		case cty.GetAttrStep:
			name, err := json.Marshal(s.Name)
			if err != nil {
				return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err)
			}
			steps = append(steps, name)
		default:
			return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step)
		}
	}
	return json.Marshal(steps)
}

View on GitHub (pinned to d32a084675)