Merge pull request #113 from hashicorp/bendbennett/issues-111

Add `useJSONNumber` to `Plan` and amend `UnmarshalJSON` behavior
diff --git a/plan.go b/plan.go
index b6583c0..04a71ca 100644
--- a/plan.go
+++ b/plan.go
@@ -4,6 +4,7 @@
 package tfjson
 
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -29,6 +30,12 @@
 
 // Plan represents the entire contents of an output Terraform plan.
 type Plan struct {
+	// useJSONNumber opts into the behavior of calling
+	// json.Decoder.UseNumber prior to decoding the plan, which turns
+	// numbers into json.Numbers instead of float64s. Set it using
+	// Plan.UseJSONNumber.
+	useJSONNumber bool
+
 	// The version of the plan format. This should always match the
 	// PlanFormatVersion constant in this package, or else an unmarshal
 	// will be unstable.
@@ -85,6 +92,14 @@
 	Attribute []json.RawMessage `json:"attribute"`
 }
 
+// UseJSONNumber controls whether the Plan will be decoded using the
+// json.Number behavior or the float64 behavior. When b is true, the Plan will
+// represent numbers in PlanOutputs as json.Numbers. When b is false, the
+// Plan will represent numbers in PlanOutputs as float64s.
+func (p *Plan) UseJSONNumber(b bool) {
+	p.useJSONNumber = b
+}
+
 // Validate checks to ensure that the plan is present, and the
 // version matches the version supported by this library.
 func (p *Plan) Validate() error {
@@ -127,7 +142,11 @@
 	type rawPlan Plan
 	var plan rawPlan
 
-	err := json.Unmarshal(b, &plan)
+	dec := json.NewDecoder(bytes.NewReader(b))
+	if p.useJSONNumber {
+		dec.UseNumber()
+	}
+	err := dec.Decode(&plan)
 	if err != nil {
 		return err
 	}
diff --git a/plan_test.go b/plan_test.go
index 2f9b37c..1f59fc1 100644
--- a/plan_test.go
+++ b/plan_test.go
@@ -120,3 +120,59 @@
 		t.Fatalf("unexpected previous address %s, expected is random_id.test", plan.ResourceChanges[0].PreviousAddress)
 	}
 }
+
+func TestPlan_UnmarshalJSON(t *testing.T) {
+	t.Parallel()
+
+	b, err := os.ReadFile("testdata/numerics/plan.json")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testCases := map[string]struct {
+		useJSONNumber bool
+		expected      any
+	}{
+		"float64": {
+			expected: 1.23,
+		},
+		"json-number": {
+			useJSONNumber: true,
+			expected:      json.Number("1.23"),
+		},
+	}
+
+	for name, testCase := range testCases {
+		name, testCase := name, testCase
+
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+
+			plan := &Plan{}
+
+			plan.UseJSONNumber(testCase.useJSONNumber)
+
+			err = plan.UnmarshalJSON(b)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			after, ok := plan.ResourceChanges[0].Change.After.(map[string]any)
+
+			if !ok {
+				t.Fatal("plan.ResourceChanges[0].Change.After cannot be asserted as map[string]any")
+			}
+
+			attr, ok := after["configurable_attribute"]
+
+			if !ok {
+				t.Fatal("configurable attribute not found")
+			}
+
+			if diff := cmp.Diff(attr, testCase.expected); diff != "" {
+				t.Errorf("unexpected difference: %s", diff)
+			}
+		})
+	}
+}
diff --git a/testdata/numerics/plan.json b/testdata/numerics/plan.json
new file mode 100644
index 0000000..ea5b5fd
--- /dev/null
+++ b/testdata/numerics/plan.json
@@ -0,0 +1 @@
+{"format_version":"1.2","terraform_version":"1.6.5","planned_values":{"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","schema_version":0,"values":{"configurable_attribute":1.23,"id":"one"},"sensitive_values":{}}]}},"resource_changes":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","change":{"actions":["create"],"before":null,"after":{"configurable_attribute":1.23,"id":"one"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"example":{"name":"example","full_name":"registry.terraform.io/hashicorp/example"}},"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_config_key":"example","expressions":{"configurable_attribute":{"constant_value":1.23},"id":{"constant_value":"one"}},"schema_version":0}]}},"timestamp":"2023-12-07T13:55:56Z"}