Compare commits

...

8 Commits

Author SHA1 Message Date
Aum Patel
78e4fe63d8
Merge e521dba4400cefab9549d4152b5dc64711c20a29 into d9e5cdf9c6f9c1643be6e081516469c71645d93d 2026-01-24 15:55:20 +05:30
dependabot[bot]
d9e5cdf9c6
chore(deps): bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1 (#4476)
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 17:55:09 +08:00
Raju Ahmed
53410d2e07
feat(context): add GetError and GetErrorSlice methods for error retrieval (#4502)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-24 17:54:37 +08:00
Bo-Yi Wu
e521dba440
Merge branch 'master' into feature/pdf-support 2026-01-24 17:41:31 +08:00
dependabot[bot]
ac95fa6bbc
chore(deps): bump goreleaser/goreleaser-action from 5 to 6 (#3992)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-24 15:22:06 +08:00
takanuva15
192ac89eef
feat(binding): add support for encoding.UnmarshalText in uri/query binding (#4203) 2026-01-24 15:20:24 +08:00
Aum Patel
1cc62143b5 render: update PDF renderer copyright header 2026-01-17 20:54:45 +05:30
Aum Patel
bc56cc086f render: add PDF renderer and tests 2026-01-04 19:24:56 +05:30
10 changed files with 587 additions and 30 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

@ -386,6 +386,11 @@ func (c *Context) GetDuration(key any) time.Duration {
return getTyped[time.Duration](c, key)
}
// GetError returns the value associated with the key as an error.
func (c *Context) GetError(key any) error {
return getTyped[error](c, key)
}
// GetIntSlice returns the value associated with the key as a slice of integers.
func (c *Context) GetIntSlice(key any) []int {
return getTyped[[]int](c, key)
@ -451,6 +456,11 @@ func (c *Context) GetStringSlice(key any) []string {
return getTyped[[]string](c, key)
}
// GetErrorSlice returns the value associated with the key as a slice of errors.
func (c *Context) GetErrorSlice(key any) []error {
return getTyped[[]error](c, key)
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key any) map[string]any {
return getTyped[map[string]any](c, key)
@ -1212,6 +1222,12 @@ func (c *Context) XML(code int, obj any) {
c.Render(code, render.XML{Data: obj})
}
// PDF writes the given PDF binary data into the response body.
// It also sets the Content-Type as "application/pdf".
func (c *Context) PDF(code int, data []byte) {
c.Render(code, render.PDF{Data: data})
}
// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj any) {
c.Render(code, render.YAML{Data: obj})

View File

@ -516,6 +516,14 @@ func TestContextGetDuration(t *testing.T) {
assert.Equal(t, time.Second, c.GetDuration("duration"))
}
func TestContextGetError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error"
value := errors.New("test error")
c.Set(key, value)
assert.Equal(t, value, c.GetError(key))
}
func TestContextGetIntSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "int-slice"
@ -618,6 +626,14 @@ func TestContextGetStringSlice(t *testing.T) {
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
}
func TestContextGetErrorSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
key := "error-slice"
value := []error{errors.New("error1"), errors.New("error2")}
c.Set(key, value)
assert.Equal(t, value, c.GetErrorSlice(key))
}
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
m := make(map[string]any)

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

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/gin-contrib/sse v1.1.0
github.com/go-playground/validator/v10 v10.28.0
github.com/goccy/go-json v0.10.2
github.com/goccy/go-yaml v1.19.0
github.com/goccy/go-yaml v1.19.1
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/modern-go/reflect2 v1.0.2

4
go.sum
View File

@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

26
render/pdf.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2026 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import "net/http"
// PDF contains the given PDF binary data.
type PDF struct {
Data []byte
}
var pdfContentType = []string{"application/pdf"}
// Render (PDF) writes PDF data with custom ContentType.
func (r PDF) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
_, err := w.Write(r.Data)
return err
}
// WriteContentType (PDF) writes PDF ContentType for response.
func (r PDF) WriteContentType(w http.ResponseWriter) {
writeContentType(w, pdfContentType)
}

View File

@ -31,6 +31,7 @@ var (
_ Render = (*AsciiJSON)(nil)
_ Render = (*ProtoBuf)(nil)
_ Render = (*TOML)(nil)
_ Render = (*PDF)(nil)
)
func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -375,6 +375,22 @@ func TestRenderXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderPDF(t *testing.T) {
w := httptest.NewRecorder()
data := []byte("%Test pdf content")
pdf := PDF{data}
pdf.WriteContentType(w)
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
err := pdf.Render(w)
require.NoError(t, err)
assert.Equal(t, data, w.Body.Bytes())
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
}
func TestRenderRedirect(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/test-redirect", nil)
require.NoError(t, err)