hashicorp / hashicorp/terraform-plugin-framework
Certain precise Number attributes cause permanent plan diff, depending on their big.Float representation
- Dominant language
- Go
- Stars
- 384
- Forks
- 107
- Avg merge
- 3m
- Merged PRs (30d)
- 1
Description
### Module version
```
github.com/hashicorp/terraform-plugin-framework v1.17.0
```
### Relevant provider source code
Source is from the `BigFloatsIssue` branch here: https://github.com/henryrecker-pingidentity/terraform-provider-example/tree/BigFloatsIssue
There is some hard-coding of the string parsing to achieve the desired effect, but the important point is that the API the provider covers returns the exact same value for the `number` attribute as the value defined in the terraform configuration. The `planNumStr` value exactly matches the config, based on stepping through with a debugger.
```go
func (r *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Example resource.",
Attributes: map[string]schema.Attribute{
"number": schema.NumberAttribute{
Required: true,
},
"computed": schema.StringAttribute{
Computed: true,
},
},
}
}
func parseNumber(str string) types.Number {
apiRespBigFloat := new(big.Float)
updatedFloat, ok := apiRespBigFloat.SetString(str)
if !ok {
panic("unable to parse number from string " + str)
}
return types.NumberValue(updatedFloat)
}
func planNumberToString(number types.Number) string {
// Hardcode to 14 to match example value for testing
// Simulate API returning exact same value from plan in JSON response
return number.ValueBigFloat().Text('f', 14)
}
// Metadata returns the resource type name.
func (r *exampleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_example"
}
func (r *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan exampleResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
planNumStr := planNumberToString(plan.Number)
numberVal := parseNumber(planNumStr)
state := exampleResourceModel{
Number: numberVal,
Computed: types.StringValue("computed value"),
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *exampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data exampleResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
planNumStr := planNumberToString(data.Number)
data.Number = parseNumber(planNumStr)
data.Computed = types.StringValue("computed value")
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *exampleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan exampleResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
planNumStr := planNumberToString(plan.Number)
numberVal := parseNumber(planNumStr)
state := exampleResourceModel{
Number: numberVal,
Computed: types.StringValue("computed value"),
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
```
### Terraform Configuration Files
```hcl
provider "example" {
}
resource "example_example" "myExample" {
# Causes infinite plans
number = 242.08120431461208
# Does not cause infinite plans - last digit changed
# number = 242.08120431461209
}
```
### Debug Output
https://gist.github.com/henryrecker-pingidentity/e855191f982ebe6a5b9a4c8b42fc4cd1
Additionally, I used `TF_LOG_SDK_PROTO_DATA_DIR` and `fq -d msgpack torepr` to print the value of `PlanResourceChange_Request_PriorState.msgpack` and `PlanResourceChange_Request_ProposedNewState.msgpack` for comparison.
Prior state:
```
{
"computed": "computed value",
"number": 242.08120431461208
}
```
Proposed state (note the quotes):
```
{
"computed": "computed value",
"number": "242.08120431461208"
}
```
### Expected Behavior
No plans generated
### Actual Behavior
Certain numbers cause permanent plan diffs. I observed that there is no plan output unless the schema includes a computed attribute. Then the computed attribute gets marked as Unknown, which causes a plan that only includes the computed attribute.
I noticed in some debug output for the provider where we ran into this bug, there is a log message about an unexpected new value after refresh, and it shows a different representation for Number values depending on the specific float value:
```
2026-02-06T10:53:48.108-0600 [WARN] Provider "registry.terraform.io/pingidentity/pingone" produced an unexpected new value for pingone_davinci_flow.example-device-registration-subflow during refresh.
...
"position":cty.ObjectVal(map[string]cty.Value{"x":cty.NumberFloatVal(243.08120431461208), "y":cty.MustParseNumberVal("3515.0994764477023")})
```
The `cty.NumberFloatVal` is the problematic number that causes plans. The `cty.MustParseNumberVal` does not cause any plans. I did a quick search on the `go-cty` repo, maybe this switch case is related? https://github.com/zclconf/go-cty/blob/da4c600729aefcf628d6b042ee439e6927d1104e/cty/value_ops.go#L60
### Steps to Reproduce
terraform apply in the linked example provider
### References
Contributor guide
Assessment
This issue has not been assessed yet.