From 6ef8cfbfb87799243d69f3592c7bfc27bf0a376b Mon Sep 17 00:00:00 2001 From: takanuva15 <6986426+takanuva15@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:29:37 -0500 Subject: [PATCH] feat(binding): add support for encoding.UnmarshalText in uri/query binding --- binding/form_mapping.go | 68 ++++-- binding/form_mapping_test.go | 390 +++++++++++++++++++++++++++++++++++ docs/doc.md | 78 ++++++- 3 files changed, 509 insertions(+), 27 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e76e7510..db6aa0dc 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "errors" "fmt" "maps" @@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag type setOptions struct { isDefaultExists bool defaultValue string + // parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`) + parser string } func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { @@ -168,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter setOpt.defaultValue = strings.ReplaceAll(v, ";", ",") } } + } else if k, v = head(opt, "="); k == "parser" { + setOpt.parser = v } } @@ -191,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) { return false, nil } +// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field. +// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this. +func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) { + switch parser { + case "encoding.TextUnmarshaler": + v, ok := value.Addr().Interface().(encoding.TextUnmarshaler) + if !ok { + return false, nil + } + return true, v.UnmarshalText([]byte(val)) + } + return false, nil +} + func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) { cfTag := field.Tag.Get("collection_format") if cfTag == "" || cfTag == "multi" { @@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error case "pipes": sep = "|" default: - return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag) + return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag) } totalLength := 0 @@ -244,7 +263,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, err } - return true, setSlice(vs, value, field) + return true, setSlice(vs, value, field, opt) case reflect.Array: if len(vs) == 0 { if !opt.isDefaultExists { @@ -267,7 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } - if ok, err = trySetCustom(vs[0], value); ok { + if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(vs[0], value); ok { return ok, err } @@ -279,27 +302,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) } - return true, setArray(vs, value, field) + return true, setArray(vs, value, field, opt) default: var val string - if !ok { + if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") { val = opt.defaultValue + } else if len(vs) > 0 { + val = vs[0] } - if len(vs) > 0 { - val = vs[0] - if val == "" { - val = opt.defaultValue - } - } - if ok, err := trySetCustom(val, value); ok { + if ok, err = trySetUsingParser(val, value, opt.parser); ok { + return ok, err + } else if ok, err = trySetCustom(val, value); ok { return ok, err } - return true, setWithProperType(val, value, field) + return true, setWithProperType(val, value, field, opt) } } -func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { +func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error { + // this if-check is required for parsing nested types like []MyId, where MyId is [12]byte + if ok, err := trySetUsingParser(val, value, opt.parser); ok { + return err + } else if ok, err = trySetCustom(val, value); ok { + return err + } + // If it is a string type, no spaces are removed, and the user data is not modified here if value.Kind() != reflect.String { val = strings.TrimSpace(val) @@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel if !value.Elem().IsValid() { value.Set(reflect.New(value.Type().Elem())) } - return setWithProperType(val, value.Elem(), field) + return setWithProperType(val, value.Elem(), field, opt) default: return errUnknownType } @@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val return nil } -func setArray(vals []string, value reflect.Value, field reflect.StructField) error { +func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { for i, s := range vals { - err := setWithProperType(s, value.Index(i), field) + err := setWithProperType(s, value.Index(i), field, opt) if err != nil { return err } @@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err return nil } -func setSlice(vals []string, value reflect.Value, field reflect.StructField) error { +func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error { slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) - err := setArray(vals, slice, field) + err := setArray(vals, slice, field, opt) if err != nil { return err } diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index e007573c..c78f7398 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -5,6 +5,7 @@ package binding import ( + "encoding" "encoding/hex" "errors" "mime/multipart" @@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) { assert.EqualValues(t, 245, s.Foo) } +func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) { + var s struct { + Foo customUnmarshalParamHex `form:"foo,default=f5"` + } + err := mappingByPtr(&s, formSource{"foo": {}}, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Foo) +} + type customUnmarshalParamType struct { Protocol string Path string @@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) { assert.Equal(t, "foo", s.FileData[1]) } +func TestMappingCustomSliceStopsWhenError(t *testing.T) { + var s struct { + FileData customPath `form:"path"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUri(t *testing.T) { + var s struct { + FileData []customPath `uri:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceForm(t *testing.T) { + var s struct { + FileData []customPath `form:"path" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + type objectID [12]byte func (o *objectID) UnmarshalParam(param string) error { @@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) { assert.Equal(t, expected, s.FileData) } +func TestMappingCustomArrayOfArrayUri(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `uri:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayForm(t *testing.T) { + id1, _ := convertTo(`664a062ac74a8ad104e0e80e`) + id2, _ := convertTo(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectID `form:"ids" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectID{id1, id2}, s.FileData) +} + +// ==== TextUnmarshaler tests START ==== + +type customUnmarshalTextHex int + +func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customUnmarshalTextHex(v) + return nil +} + +// verify type implements TextUnmarshaler +var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil) + +func TestMappingCustomUnmarshalTextHexUri(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexForm(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 245, s.Field) +} + +func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) { + var s struct { + Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"field1": {}}, "form") + require.NoError(t, err) + assert.EqualValues(t, 0xf5, s.Field) +} + +type customUnmarshalTextType struct { + Protocol string + Path string + Name string +} + +func (f *customUnmarshalTextType) UnmarshalText(text []byte) error { + parts := strings.Split(string(text), ":") + if len(parts) != 3 { + return errors.New("invalid format") + } + f.Protocol = parts[0] + f.Path = parts[1] + f.Name = parts[2] + return nil +} + +var _ encoding.TextUnmarshaler = (*customUnmarshalTextType)(nil) + +func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +func TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) { + var s struct { + FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "file", s.FileData.Protocol) + assert.Equal(t, "/foo", s.FileData.Path) + assert.Equal(t, "happiness", s.FileData.Name) +} + +type customPathUnmarshalText []string + +func (p *customPathUnmarshalText) UnmarshalText(text []byte) error { + elems := strings.Split(string(text), "/") + n := len(elems) + if n < 2 { + return errors.New("invalid format") + } + + *p = elems + return nil +} + +var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil) + +func TestMappingCustomSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form") + require.NoError(t, err) + + assert.Equal(t, "bar", s.FileData[0]) + assert.Equal(t, "foo", s.FileData[1]) +} + +func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) { + var s struct { + FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form") + require.ErrorContains(t, err, "invalid format") + require.Empty(t, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) { + var s struct { + FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"path": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData) +} + +type objectIDUnmarshalText [12]byte + +func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error { + oid, err := convertToOidUnmarshalText(string(text)) + if err != nil { + return err + } + + *o = oid + return nil +} + +func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) { + oid, err := convertTo(s) + return objectIDUnmarshalText(oid), err +} + +var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil) + +func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "uri") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) { + var s struct { + FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"` + } + val := `664a062ac74a8ad104e0e80f` + err := mappingByPtr(&s, formSource{"id": {val}}, "form") + require.NoError(t, err) + + expected, _ := convertToOidUnmarshalText(val) + assert.Equal(t, expected, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) { + id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`) + id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`) + + var s struct { + FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + err := mappingByPtr(&s, formSource{"ids": {}}, "form") + require.NoError(t, err) + assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData) +} + +// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default +// binding logic. +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalParamHex `form:"hex"` + HexByUnmarshalText customUnmarshalParamHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + "hex2": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) + assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding +} + +// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the +// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards +// compatibility) +func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) { + var s struct { + Hex customUnmarshalTextHex `form:"hex"` + HexByUnmarshalText customUnmarshalTextHex `form:"hex2,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`11`}, + "hex2": {`11`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal + assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for normal hex binding +} + +type customHexUnmarshalParamAndUnmarshalText int + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error { + return errors.New("should not be called in unit test if parser tag present") +} + +func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error { + v, err := strconv.ParseInt(string(text), 16, 64) + if err != nil { + return err + } + *f = customHexUnmarshalParamAndUnmarshalText(v) + return nil +} + +// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler, +// then only the UnmarshalText method should be invoked. +func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) { + var s struct { + Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"` + } + err := mappingByPtr(&s, formSource{ + "hex": {`f5`}, + }, "form") + require.NoError(t, err) + + assert.EqualValues(t, 0xf5, s.Hex) +} + +// ==== TextUnmarshaler tests END ==== + func TestMappingEmptyValues(t *testing.T) { t.Run("slice with default", func(t *testing.T) { var s struct { diff --git a/docs/doc.md b/docs/doc.md index 0dd86684..449c8d02 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -911,7 +911,7 @@ curl -X POST http://localhost:8080/person NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply: - Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior -- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values +- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values - Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv" @@ -1009,12 +1009,68 @@ curl -v localhost:8088/thinkerou/not-uuid ### Bind custom unmarshaler +To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound. + ```go package main import ( - "github.com/gin-gonic/gin" + "encoding" "strings" + + "github.com/gin-gonic/gin" +) + +type Birthday string + +func (b *Birthday) UnmarshalText(text []byte) error { + *b = Birthday(strings.Replace(string(text), "-", "/", -1)) + return nil +} + +var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler + +func main() { + route := gin.Default() + var request struct { + Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"` + Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"` + } + route.GET("/test", func(ctx *gin.Context) { + _ = ctx.BindQuery(&request) + ctx.JSON(200, request) + }) + _ = route.Run(":8088") +} +``` + +Test it with: + +```sh +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' +``` +Result +```sh +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} +``` + +Note: +- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic. +- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client. + +--- + +If a type already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead. + +```go +package main + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) type Birthday string @@ -1024,29 +1080,37 @@ func (b *Birthday) UnmarshalParam(param string) error { return nil } +var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler + func main() { route := gin.Default() var request struct { - Birthday Birthday `form:"birthday"` + Birthday Birthday `form:"birthday"` + Birthdays []Birthday `form:"birthdays" collection_format:"csv"` + BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"` } route.GET("/test", func(ctx *gin.Context) { _ = ctx.BindQuery(&request) - ctx.JSON(200, request.Birthday) + ctx.JSON(200, request) }) - route.Run(":8088") + _ = route.Run(":8088") } ``` Test it with: ```sh -curl 'localhost:8088/test?birthday=2000-01-01' +curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02' ``` Result ```sh -"2000/01/01" +{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]} ``` +Note: +- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag. +- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client. + ### Bind Header ```go