Repository: stripe/stripe-go (go)

Issue:
Unable to remove product attributes
I am working on a [Terraform provider](https://github.com/franckverrot/terraform-provider-stripe/pull/3), and am unable to delete product attributes. My test below demonstrates this.

Is this a bug in the client library, or am is my implementation incorrect (totally feasible given this is my first foray into Go)?

Go version: `go version go1.11.1 darwin/amd64`
Client version: `55.14.0`

```go
import (
	"github.com/stripe/stripe-go"
	"github.com/stripe/stripe-go/client"
	"log"
	"testing"
)

func TestAttributeRemoval(t *testing.T) {
	sc := &client.API{}
	sc.Init("REPLACE-ME", nil)

	tmp := "foo"
	attributes := []*string{&tmp}


	params := stripe.ProductParams{
		Name: stripe.String("Attribute Test"),
		Type: stripe.String("good"),
		Attributes: attributes,
	}

	product, err := sc.Products.New(&params)

	if err != nil {
		t.Errorf("Product creation failed: %v", err)
	}

	params = stripe.ProductParams{
		Attributes: []*string{},
	}
	product, err = sc.Products.Update(product.ID, &params)

	if err != nil {
		t.Errorf("Product update failed: %v", err)
	}

	log.Printf("%v", product)

	if len(product.Attributes) != 0 {
		t.Errorf("Attributes were not deleted: %v", product.Attributes)
	}
}
```

Pull request:
Allow API arrays to be emptied by setting an empty array
Allows API arrays like `additional_owners` to be emptied by setting
their value to an explicit empty array. To accomplish this we need to
rework parts of the form-encoding library to detect this specific
condition.

I also had to rework a fair bit of the form package's test suite because
the existing code was making no distinction between an actual value
being set an empty in the generated form versus one that's not present.

Fixes #788.

r? @remi-stripe

Patch:
diff --git a/charge.go b/charge.go
index 4f3352b6..e17b1873 100644
--- a/charge.go
+++ b/charge.go
@@ -43,7 +43,6 @@ type ChargeLevel3Params struct {
 
 // ChargeTransferDataParams is the set of parameters allowed for the transfer_data hash.
 type ChargeTransferDataParams struct {
-	Amount      *int64  `form:"amount"`
 	Destination *string `form:"destination"`
 }
 
@@ -146,7 +145,6 @@ type ChargeLevel3 struct {
 
 // ChargeTransferData represents the information for the transfer_data associated with a charge.
 type ChargeTransferData struct {
-	Amount      int64    `form:"amount"`
 	Destination *Account `json:"destination"`
 }
 
diff --git a/form/form.go b/form/form.go
index 80e31124..d2726e4e 100644
--- a/form/form.go
+++ b/form/form.go
@@ -153,6 +153,19 @@ func buildArrayOrSliceEncoder(t reflect.Type) encoderFunc {
 	elemF := getCachedOrBuildTypeEncoder(t.Elem())
 
 	return func(values *Values, v reflect.Value, keyParts []string, _ bool, options *formOptions) {
+		// When encountering a slice that's been explicitly set (i.e. non-nil)
+		// and which is of 0 length, we take this as an indication that the
+		// user is trying to zero the API array. See the `additional_owners`
+		// property under `legal_entity` on account for an example of somewhere
+		// that this is useful.
+		//
+		// This only works for a slice (and not an array) because even a zeroed
+		// array always has a fixed length.
+		if t.Kind() == reflect.Slice && !v.IsNil() && v.Len() == 0 {
+			values.Add(FormatKey(keyParts), "")
+			return
+		}
+
 		var arrNames []string
 
 		for i := 0; i < v.Len(); i++ {
@@ -173,9 +186,20 @@ func buildPtrEncoder(t reflect.Type) encoderFunc {
 	elemF := getCachedOrBuildTypeEncoder(t.Elem())
 
 	return func(values *Values, v reflect.Value, keyParts []string, _ bool, options *formOptions) {
+		// We take a nil to mean that the property wasn't set, so ignore it in
+		// the final encoding.
 		if v.IsNil() {
 			return
 		}
+
+		// Handle "zeroing" an array stored as a pointer to a slice. See
+		// comment in `buildArrayOrSliceEncoder` above.
+		if t.Elem().Kind() == reflect.Slice && v.Elem().Len() == 0 {
+			values.Add(FormatKey(keyParts), "")
+			return
+		}
+
+		// Otherwise, call into the appropriate encoder for the pointer's type.
 		elemF(values, v.Elem(), keyParts, true, options)
 	}
 }
diff --git a/invoice.go b/invoice.go
index 9a6e3592..5ce46b1c 100644
--- a/invoice.go
+++ b/invoice.go
@@ -72,30 +72,24 @@ type InvoiceCustomFieldParams struct {
 	Value *string `form:"value"`
 }
 
-// InvoiceTransferDataParams is the set of parameters allowed for the transfer_data hash.
-type InvoiceTransferDataParams struct {
-	Destination *string `form:"destination"`
-}
-
 // InvoiceParams is the set of parameters that can be used when creating or updating an invoice.
 // For more details see https://stripe.com/docs/api#create_invoice, https://stripe.com/docs/api#update_invoice.
 type InvoiceParams struct {
-	Params               `form:"*"`
-	AutoAdvance          *bool                       `form:"auto_advance"`
-	ApplicationFeeAmount *int64                      `form:"application_fee_amount"`
-	Billing              *string                     `form:"billing"`
-	CustomFields         []*InvoiceCustomFieldParams `form:"custom_fields"`
-	Customer             *string                     `form:"customer"`
-	DaysUntilDue         *int64                      `form:"days_until_due"`
-	DefaultSource        *string                     `form:"default_source"`
-	Description          *string                     `form:"description"`
-	DueDate              *int64                      `form:"due_date"`
-	Footer               *string                     `form:"footer"`
-	Paid                 *bool                       `form:"paid"`
-	StatementDescriptor  *string                     `form:"statement_descriptor"`
-	Subscription         *string                     `form:"subscription"`
-	TaxPercent           *float64                    `form:"tax_percent"`
-	TransferData         *InvoiceTransferDataParams  `form:"transfer_data"`
+	Params              `form:"*"`
+	AutoAdvance         *bool                       `form:"auto_advance"`
+	ApplicationFee      *int64                      `form:"application_fee"`
+	Billing             *string                     `form:"billing"`
+	CustomFields        []*InvoiceCustomFieldParams `form:"custom_fields"`
+	Customer            *string                     `form:"customer"`
+	DaysUntilDue        *int64                      `form:"days_until_due"`
+	DefaultSource       *string                     `form:"default_source"`
+	Description         *string                     `form:"description"`
+	DueDate             *int64                      `form:"due_date"`
+	Footer              *string                     `form:"footer"`
+	Paid                *bool                       `form:"paid"`
+	StatementDescriptor *string                     `form:"statement_descriptor"`
+	Subscription        *string                     `form:"subscription"`
+	TaxPercent          *float64                    `form:"tax_percent"`
 
 	// These are all for exclusive use by GetNext.
 
@@ -111,9 +105,6 @@ type InvoiceParams struct {
 	SubscriptionTaxPercent         *float64                          `form:"subscription_tax_percent"`
 	SubscriptionTrialEnd           *int64                            `form:"subscription_trial_end"`
 	SubscriptionTrialFromPlan      *bool                             `form:"subscription_trial_from_plan"`
-
-	// This parameter is considered deprecated. Prefer using ApplicationFeeAmount
-	ApplicationFee *int64 `form:"application_fee"`
 }
 
 // InvoiceListParams is the set of parameters that can be used when listing invoices.
@@ -179,7 +170,7 @@ type Invoice struct {
 	AmountDue                 int64                   `json:"amount_due"`
 	AmountPaid                int64                   `json:"amount_paid"`
 	AmountRemaining           int64                   `json:"amount_remaining"`
-	ApplicationFeeAmount      int64                   `json:"application_fee_amount"`
+	ApplicationFee            int64                   `json:"application_fee"`
 	AttemptCount              int64                   `json:"attempt_count"`
 	Attempted                 bool                    `json:"attempted"`
 	AutoAdvance               bool                    `json:"auto_advance"`
@@ -219,11 +210,7 @@ type Invoice struct {
 	TaxPercent                float64                 `json:"tax_percent"`
 	ThreasholdReason          *InvoiceThresholdReason `json:"threshold_reason"`
 	Total                     int64                   `json:"total"`
-	TransferData              *InvoiceTransferData    `json:"transfer_data"`
 	WebhooksDeliveredAt       int64                   `json:"webhooks_delivered_at"`
-
-	// This property is considered deprecated. Prefer using ApplicationFeeAmount
-	ApplicationFee int64 `json:"application_fee"`
 }
 
 // InvoiceCustomField is a structure representing a custom field on an Invoice.
@@ -270,11 +257,6 @@ type InvoiceLine struct {
 	Type             InvoiceLineType   `json:"type"`
 }
 
-// InvoiceTransferData represents the information for the transfer_data associated with an invoice.
-type InvoiceTransferData struct {
-	Destination *Account `json:"destination"`
-}
-
 // Period is a structure representing a start and end dates.
 type Period struct {
 	End   int64 `json:"end"`
diff --git a/sub.go b/sub.go
index 6da763bf..06782555 100644
--- a/sub.go
+++ b/sub.go
@@ -28,11 +28,6 @@ const (
 	SubscriptionBillingSendInvoice         SubscriptionBilling = "send_invoice"
 )
 
-// SubscriptionTransferDataParams is the set of parameters allowed for the transfer_data hash.
-type SubscriptionTransferDataParams struct {
-	Destination *string `form:"destination"`
-}
-
 // SubscriptionParams is the set of parameters that can be used when creating or updating a subscription.
 // For more details see https://stripe.com/docs/api#create_subscription and https://stripe.com/docs/api#update_subscription.
 type SubscriptionParams struct {
@@ -57,7 +52,6 @@ type SubscriptionParams struct {
 	Quantity                    *int64                               `form:"quantity"`
 	TaxPercent                  *float64                             `form:"tax_percent"`
 	TrialEnd                    *int64                               `form:"trial_end"`
-	TransferData                *SubscriptionTransferDataParams      `form:"transfer_data"`
 	TrialEndNow                 *bool                                `form:"-"` // See custom AppendTo
 	TrialFromPlan               *bool                                `form:"trial_from_plan"`
 	TrialPeriodDays             *int64                               `form:"trial_period_days"`
@@ -118,11 +112,6 @@ type SubscriptionListParams struct {
 	Status       string            `form:"status"`
 }
 
-// SubscriptionTransferData represents the information for the transfer_data associated with a subscription.
-type SubscriptionTransferData struct {
-	Destination *Account `json:"destination"`
-}
-
 // Subscription is the resource representing a Stripe subscription.
 // For more details see https://stripe.com/docs/api#subscriptions.
 type Subscription struct {
@@ -149,7 +138,6 @@ type Subscription struct {
 	Start                 int64                          `json:"start"`
 	Status                SubscriptionStatus             `json:"status"`
 	TaxPercent            float64                        `json:"tax_percent"`
-	TransferData          *SubscriptionTransferData      `json:"transfer_data"`
 	TrialEnd              int64                          `json:"trial_end"`
 	TrialStart            int64                          `json:"trial_start"`
 }


Test patch:
diff --git a/form/form_test.go b/form/form_test.go
index 0519d3e6..34b64408 100644
--- a/form/form_test.go
+++ b/form/form_test.go
@@ -1,6 +1,7 @@
 package form
 
 import (
+	"fmt"
 	"net/url"
 	"sync"
 	"testing"
@@ -118,6 +119,7 @@ func BenchmarkAppendTo(b *testing.B) {
 
 func TestAppendTo(t *testing.T) {
 	var arrayVal = [3]string{"1", "2", "3"}
+	var arrayVal0 = [3]string{}
 
 	var boolValT = true
 	var boolValF = false
@@ -140,6 +142,7 @@ func TestAppendTo(t *testing.T) {
 	var int64Val0 int64
 
 	var sliceVal = []string{"1", "2", "3"}
+	var sliceVal0 = []string{}
 
 	var stringVal = "123"
 	var stringVal0 = ""
@@ -164,49 +167,56 @@ func TestAppendTo(t *testing.T) {
 	testCases := []struct {
 		field string
 		data  *testStruct
-		want  interface{}
+
+		// Set to a pointer to the desired value or nil if the value shouldn't
+		// be present.
+		want *string
 	}{
-		{"appender", &testStruct{Appender: &testAppender{String: "123"}}, "123"},
-
-		{"array[2]", &testStruct{Array: arrayVal}, "3"},
-
-		{"bool", &testStruct{Bool: boolValT}, "true"},
-		{"bool_ptr", &testStruct{}, ""},
-		{"bool_ptr", &testStruct{BoolPtr: &boolValT}, "true"},
-		{"bool_ptr", &testStruct{BoolPtr: &boolValF}, "false"},
-
-		{"emptied", &testStruct{Emptied: true}, ""},
-
-		{"float32", &testStruct{Float32: float32Val}, "1.2345"},
-		{"float32_ptr", &testStruct{Float32Ptr: &float32Val}, "1.2345"},
-		{"float32_ptr", &testStruct{Float32Ptr: &float32Val0}, "0.0000"},
-		{"float32_ptr", &testStruct{}, ""},
-
-		{"float64", &testStruct{Float64: float64Val}, "1.2345"},
-		{"float64_ptr", &testStruct{Float64Ptr: &float64Val}, "1.2345"},
-		{"float64_ptr", &testStruct{Float64Ptr: &float64Val0}, "0.0000"},
-		{"float64_ptr", &testStruct{}, ""},
-
-		{"int", &testStruct{Int: intVal}, "123"},
-		{"int_ptr", &testStruct{IntPtr: &intVal}, "123"},
-		{"int_ptr", &testStruct{IntPtr: &intVal0}, "0"},
-		{"int_ptr", &testStruct{}, ""},
-		{"int8", &testStruct{Int8: int8Val}, "123"},
-		{"int8_ptr", &testStruct{Int8Ptr: &int8Val}, "123"},
-		{"int8_ptr", &testStruct{Int8Ptr: &int8Val0}, "0"},
-		{"int8_ptr", &testStruct{}, ""},
-		{"int16", &testStruct{Int16: int16Val}, "123"},
-		{"int16_ptr", &testStruct{Int16Ptr: &int16Val}, "123"},
-		{"int16_ptr", &testStruct{Int16Ptr: &int16Val0}, "0"},
-		{"int16_ptr", &testStruct{}, ""},
-		{"int32", &testStruct{Int32: int32Val}, "123"},
-		{"int32_ptr", &testStruct{Int32Ptr: &int32Val}, "123"},
-		{"int32_ptr", &testStruct{Int32Ptr: &int32Val0}, "0"},
-		{"int32_ptr", &testStruct{}, ""},
-		{"int64", &testStruct{Int64: int64Val}, "123"},
-		{"int64_ptr", &testStruct{Int64Ptr: &int64Val}, "123"},
-		{"int64_ptr", &testStruct{Int64Ptr: &int64Val0}, "0"},
-		{"int64_ptr", &testStruct{}, ""},
+		{"appender", &testStruct{Appender: &testAppender{String: "123"}}, stringPtr("123")},
+
+		{"array[2]", &testStruct{Array: arrayVal}, stringPtr("3")},
+		{"array", &testStruct{Array: arrayVal0}, nil},
+
+		{"array_ptr[2]", &testStruct{ArrayPtr: &arrayVal}, stringPtr("3")},
+		{"array_ptr", &testStruct{ArrayPtr: &arrayVal0}, nil},
+
+		{"bool", &testStruct{Bool: boolValT}, stringPtr("true")},
+		{"bool_ptr", &testStruct{}, nil},
+		{"bool_ptr", &testStruct{BoolPtr: &boolValT}, stringPtr("true")},
+		{"bool_ptr", &testStruct{BoolPtr: &boolValF}, stringPtr("false")},
+
+		{"emptied", &testStruct{Emptied: true}, stringPtr("")},
+
+		{"float32", &testStruct{Float32: float32Val}, stringPtr("1.2345")},
+		{"float32_ptr", &testStruct{Float32Ptr: &float32Val}, stringPtr("1.2345")},
+		{"float32_ptr", &testStruct{Float32Ptr: &float32Val0}, stringPtr("0.0000")},
+		{"float32_ptr", &testStruct{}, nil},
+
+		{"float64", &testStruct{Float64: float64Val}, stringPtr("1.2345")},
+		{"float64_ptr", &testStruct{Float64Ptr: &float64Val}, stringPtr("1.2345")},
+		{"float64_ptr", &testStruct{Float64Ptr: &float64Val0}, stringPtr("0.0000")},
+		{"float64_ptr", &testStruct{}, nil},
+
+		{"int", &testStruct{Int: intVal}, stringPtr("123")},
+		{"int_ptr", &testStruct{IntPtr: &intVal}, stringPtr("123")},
+		{"int_ptr", &testStruct{IntPtr: &intVal0}, stringPtr("0")},
+		{"int_ptr", &testStruct{}, nil},
+		{"int8", &testStruct{Int8: int8Val}, stringPtr("123")},
+		{"int8_ptr", &testStruct{Int8Ptr: &int8Val}, stringPtr("123")},
+		{"int8_ptr", &testStruct{Int8Ptr: &int8Val0}, stringPtr("0")},
+		{"int8_ptr", &testStruct{}, nil},
+		{"int16", &testStruct{Int16: int16Val}, stringPtr("123")},
+		{"int16_ptr", &testStruct{Int16Ptr: &int16Val}, stringPtr("123")},
+		{"int16_ptr", &testStruct{Int16Ptr: &int16Val0}, stringPtr("0")},
+		{"int16_ptr", &testStruct{}, nil},
+		{"int32", &testStruct{Int32: int32Val}, stringPtr("123")},
+		{"int32_ptr", &testStruct{Int32Ptr: &int32Val}, stringPtr("123")},
+		{"int32_ptr", &testStruct{Int32Ptr: &int32Val0}, stringPtr("0")},
+		{"int32_ptr", &testStruct{}, nil},
+		{"int64", &testStruct{Int64: int64Val}, stringPtr("123")},
+		{"int64_ptr", &testStruct{Int64Ptr: &int64Val}, stringPtr("123")},
+		{"int64_ptr", &testStruct{Int64Ptr: &int64Val0}, stringPtr("0")},
+		{"int64_ptr", &testStruct{}, nil},
 
 		// Tests map
 		{
@@ -214,7 +224,7 @@ func TestAppendTo(t *testing.T) {
 			&testStruct{Map: map[string]interface{}{
 				"foo": "bar",
 			}},
-			"bar",
+			stringPtr("bar"),
 		},
 
 		// Tests map with an empty value
@@ -228,7 +238,7 @@ func TestAppendTo(t *testing.T) {
 				// value for int64 is 0, so we can.
 				"empty": int64(0),
 			}},
-			"0",
+			stringPtr("0"),
 		},
 
 		// Tests map nested inside of another map
@@ -237,50 +247,86 @@ func TestAppendTo(t *testing.T) {
 			&testStruct{Map: map[string]interface{}{
 				"foo": map[string]interface{}{"bar": "baz"},
 			}},
-			"baz",
+			stringPtr("baz"),
 		},
 
-		{"slice[2]", &testStruct{Slice: sliceVal}, "3"},
-
-		{"string", &testStruct{String: stringVal}, stringVal},
-		{"string_ptr", &testStruct{StringPtr: &stringVal}, stringVal},
-		{"string_ptr", &testStruct{StringPtr: &stringVal0}, stringVal0},
-		{"string_ptr", &testStruct{}, stringVal0},
-
-		{"substruct[subsubstruct][string]", &testStruct{SubStruct: subStructVal}, "123"},
-		{"substruct_ptr[subsubstruct][string]", &testStruct{SubStructPtr: &subStructVal}, "123"},
-
-		{"subsubstruct[string]", &testStruct{SubStructFlat: subStructVal}, "123"},
-		{"subsubstruct[string]", &testStruct{SubStructFlatPtr: &subStructVal}, "123"},
-
-		{"uint", &testStruct{Uuint: uintVal}, "123"},
-		{"uint_ptr", &testStruct{UuintPtr: &uintVal}, "123"},
-		{"uint_ptr", &testStruct{UuintPtr: &uintVal0}, "0"},
-		{"uint_ptr", &testStruct{}, ""},
-		{"uint8", &testStruct{Uuint8: uint8Val}, "123"},
-		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val}, "123"},
-		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val0}, "0"},
-		{"uint8_ptr", &testStruct{}, ""},
-		{"uint16", &testStruct{Uuint16: uint16Val}, "123"},
-		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val}, "123"},
-		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val0}, "0"},
-		{"uint16_ptr", &testStruct{}, ""},
-		{"uint32", &testStruct{Uuint32: uint32Val}, "123"},
-		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val}, "123"},
-		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val0}, "0"},
-		{"uint32_ptr", &testStruct{}, ""},
-		{"uint64", &testStruct{Uuint64: uint64Val}, "123"},
-		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val}, "123"},
-		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val0}, "0"},
-		{"uint64_ptr", &testStruct{}, ""},
+		{"slice[2]", &testStruct{Slice: sliceVal}, stringPtr("3")},
+		{"slice", &testStruct{Slice: []string{}}, stringPtr("")},
+		{"slice", &testStruct{Slice: nil}, nil},
+
+		{"slice_ptr[2]", &testStruct{SlicePtr: &sliceVal}, stringPtr("3")},
+
+		// A slice pointer given an explicit but empty slice should encode to
+		// an empty string (this tells the Stripe API to "zero" the array).
+		{"slice_ptr", &testStruct{SlicePtr: &sliceVal0}, stringPtr("")},
+
+		{"slice_ptr", &testStruct{SlicePtr: nil}, nil},
+
+		{"string", &testStruct{String: stringVal}, &stringVal},
+		{"string_ptr", &testStruct{StringPtr: &stringVal}, &stringVal},
+		{"string_ptr", &testStruct{StringPtr: &stringVal0}, &stringVal0},
+		{"string_ptr", &testStruct{}, nil},
+
+		{"substruct[subsubstruct][string]", &testStruct{SubStruct: subStructVal}, stringPtr("123")},
+		{"substruct_ptr[subsubstruct][string]", &testStruct{SubStructPtr: &subStructVal}, stringPtr("123")},
+
+		{"subsubstruct[string]", &testStruct{SubStructFlat: subStructVal}, stringPtr("123")},
+		{"subsubstruct[string]", &testStruct{SubStructFlatPtr: &subStructVal}, stringPtr("123")},
+
+		{"uint", &testStruct{Uuint: uintVal}, stringPtr("123")},
+		{"uint_ptr", &testStruct{UuintPtr: &uintVal}, stringPtr("123")},
+		{"uint_ptr", &testStruct{UuintPtr: &uintVal0}, stringPtr("0")},
+		{"uint_ptr", &testStruct{}, nil},
+		{"uint8", &testStruct{Uuint8: uint8Val}, stringPtr("123")},
+		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val}, stringPtr("123")},
+		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val0}, stringPtr("0")},
+		{"uint8_ptr", &testStruct{}, nil},
+		{"uint16", &testStruct{Uuint16: uint16Val}, stringPtr("123")},
+		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val}, stringPtr("123")},
+		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val0}, stringPtr("0")},
+		{"uint16_ptr", &testStruct{}, nil},
+		{"uint32", &testStruct{Uuint32: uint32Val}, stringPtr("123")},
+		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val}, stringPtr("123")},
+		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val0}, stringPtr("0")},
+		{"uint32_ptr", &testStruct{}, nil},
+		{"uint64", &testStruct{Uuint64: uint64Val}, stringPtr("123")},
+		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val}, stringPtr("123")},
+		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val0}, stringPtr("0")},
+		{"uint64_ptr", &testStruct{}, nil},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.field, func(t *testing.T) {
 			form := &Values{}
 			AppendTo(form, tc.data)
 			values := form.ToValues()
-			t.Logf("values = %+v", values)
-			assert.Equal(t, tc.want, values.Get(tc.field))
+			t.Logf("values: %+v encoded: \"%+v\"", values, form.Encode())
+
+			actuals, ok := values[tc.field]
+			if ok {
+				if tc.want == nil {
+					assert.Fail(t, fmt.Sprintf(
+						"Value not expected for %s, but got '%+v'",
+						tc.field, actuals))
+				}
+			} else {
+				if tc.want == nil {
+					// Got no value and expected no value
+					return
+				}
+
+				assert.Fail(t, fmt.Sprintf(
+					"No value present for %s, but expected '%+v'",
+					tc.field, *tc.want))
+			}
+
+			if len(actuals) > 1 {
+				assert.Fail(t, fmt.Sprintf(
+					"Got more than one value for %s: %+v",
+					tc.field, actuals))
+			}
+
+			actual := actuals[0]
+			assert.Equal(t, *tc.want, actual)
 		})
 	}
 }
@@ -414,3 +460,13 @@ func TestValues(t *testing.T) {
 
 	assert.Nil(t, values.Get("boguskey"))
 }
+
+//
+// Private functions
+//
+
+// Trivial helper to get is a string pointer from a string (because you're not
+// allowed to take the address of a literal directly).
+func stringPtr(s string) *string {
+	return &s
+}