Merge 6ef8cfbfb87799243d69f3592c7bfc27bf0a376b into 9914178584e42458ff7d23891463a880f58c9d86

This commit is contained in:
takanuva15 2026-01-04 03:31:06 +00:00 committed by GitHub
commit 49ca45a5e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 509 additions and 27 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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