From c3eb208d34bbf86e19ac1d8c5db5f39c8fa38060 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 28 Oct 2026 01:20:02 +0900 Subject: [PATCH] fix: bind uuid.UUID fields in deepObject parameters uuid.UUID is a [16]byte array, so it never reached the TextUnmarshaler check that #45 added to the struct branch and still failed with "unhandled type: uuid.UUID". Handle array kinds that implement encoding.TextUnmarshaler, as BindStringToObject already does. --- deepobject.go | 8 +++++++- deepobject_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/deepobject.go b/deepobject.go index f33a020..967015d 100644 --- a/deepobject.go +++ b/deepobject.go @@ -289,6 +289,12 @@ func assignPathValues(dst interface{}, pathValues fieldOrValue) error { } iv.Set(dstSlice) return nil + case reflect.Array: + // Array-backed text types such as uuid.UUID. + if tu, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return tu.UnmarshalText([]byte(pathValues.value)) + } + return errors.New("unhandled type: " + it.String()) case reflect.Struct: // Some special types we care about are structs. Handle them // here. They may be redefined, so we need to do some hoop @@ -338,7 +344,7 @@ func assignPathValues(dst interface{}, pathValues fieldOrValue) error { dst.Set(reflect.ValueOf(tm)) return nil } - // For other struct types that implement TextUnmarshaler (e.g. uuid.UUID), + // For other struct types that implement TextUnmarshaler, // use that for binding. This comes after the legacy Date/time.Time checks // above which have special fallback format handling. if tu, ok := v.Interface().(encoding.TextUnmarshaler); ok { diff --git a/deepobject_test.go b/deepobject_test.go index 41230c1..faa5449 100644 --- a/deepobject_test.go +++ b/deepobject_test.go @@ -8,6 +8,7 @@ import ( "time" "unicode" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -561,3 +562,45 @@ func TestDeepObject_InterfaceRoundTrip(t *testing.T) { var dst map[string]interface{} assertDeepObjectWireSafe(t, marshaled, "properties", &dst, src) } + +func TestDeepObject_UUIDFields(t *testing.T) { + type UUIDObject struct { + ID uuid.UUID `json:"id"` + Owner *uuid.UUID `json:"owner,omitempty"` + } + id := uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + owner := uuid.MustParse("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + + t.Run("uuid values bind", func(t *testing.T) { + params := url.Values{} + params.Set("p[id]", id.String()) + params.Set("p[owner]", owner.String()) + + var dst UUIDObject + err := UnmarshalDeepObject(&dst, "p", params) + require.NoError(t, err) + assert.Equal(t, UUIDObject{ID: id, Owner: &owner}, dst) + }) + + t.Run("invalid uuid returns error", func(t *testing.T) { + params := url.Values{} + params.Set("p[id]", "not-a-uuid") + + var dst UUIDObject + err := UnmarshalDeepObject(&dst, "p", params) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid UUID") + }) + + t.Run("array without TextUnmarshaler is unhandled", func(t *testing.T) { + params := url.Values{} + params.Set("p[pair]", "a") + + var dst struct { + Pair [2]string `json:"pair"` + } + err := UnmarshalDeepObject(&dst, "p", params) + require.Error(t, err) + assert.Contains(t, err.Error(), "unhandled type") + }) +}