hashicorp/terraform · error

argument must be a string

Error message

argument must be a string

What it means

decode_tfvars builtin guard: the schema declares its single parameter as cty.String, so the runtime should reject non-string inputs first. This branch fires only when the function is invoked with a value whose cty type is not String, again a defensive check for direct/programmatic misuse.

Solutions

  1. Pass a string to decode_tfvars (decode_tfvars("key = \"value\"")) — coerce with tostring() if needed.
  2. Programmatic callers must pass a cty.String value.
  3. Verify the function's ParameterTypes when wrapping the function.

Example fix

// before
locals { v = decode_tfvars({a = 1}) }
// after
locals { v = decode_tfvars("a = 1\n") }
Defensive patterns

Strategy: type-guard

Validate before calling

if args[0].Type() != cty.String {
    // coerce with tostring() at the HCL level, or fail fast here
    return cty.NilVal, fmt.Errorf("decode_tfvars requires a string, got %s", args[0].Type().FriendlyName())
}

Type guard

func decodeTfvarsArgIsString(args []cty.Value) bool {
    return len(args) == 1 && args[0].Type() == cty.String
}

Prevention

When it happens

Trigger: decodeTfvarsFunc invoked with args[0].Type() != cty.String: a direct call passing a number/list/object, or a dispatcher regression that skips schema type-checking.

Common situations: Programmatic calls with the wrong cty type; a regression in schema-driven type narrowing; experimental code passing dynamic-typed values directly.

Related errors


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

Appendix: source

Thrown at internal/builtin/providers/terraform/functions.go:96

		body.SetAttributeValue(key, v)
	}

	result := f.Bytes()
	return cty.StringVal(string(result)), nil
}

func decodeTfvarsFunc(args []cty.Value) (cty.Value, error) {
	// These error checks should not be hit in practice because the language
	// runtime should check them before calling, so this is just for robustness
	// and completeness.
	if len(args) > 1 {
		return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected")
	}
	if len(args) == 0 {
		return cty.NilVal, fmt.Errorf("exactly one argument is required")
	}
	if args[0].Type() != cty.String {
		return cty.NilVal, fmt.Errorf("argument must be a string")
	}
	if args[0].IsNull() {
		return cty.NilVal, fmt.Errorf("cannot decode tfvars from a null value")
	}
	if !args[0].IsKnown() {
		// If our input isn't known then we can't even predict the result
		// type, since it will be an object type decided based on which
		// arguments and values we find in the string.
		return cty.DynamicVal, nil
	}

	// If we get here then we know that:
	// - there's exactly one element in args
	// - it's a string
	// - it is known and non-null
	// So therefore the following is guaranteed to succeed.
	src := []byte(args[0].AsString())

View on GitHub (pinned to d32a084675)