Compare commits

...

6 Commits

Author SHA1 Message Date
takanuva15
97d467d37d
Merge be4fe9b79343bf9474befb0e87c1b8c41afbd5c9 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9 2026-08-23 00:22:19 -07:00
Amirhf
dcaa4296d1
docs(path): fix malformed comment in cleanPath (#4723) 2026-08-15 13:44:19 +08:00
Gaurav Patil
8dd20118ba
fix(deps): bump golang.org/x/net and golang.org/x/text to patched versions (#4807)
The scheduled Trivy scan has been failing on master with two HIGH
findings, and it fails every pull request along with it:

- CVE-2026-56852, golang.org/x/text, denial of service, fixed in 0.39.0
- CVE-2026-46600, golang.org/x/net/dns/dnsmessage, denial of service,
  fixed in 0.56.0

Bump both to the first patched release. golang.org/x/crypto and
golang.org/x/sys move with them as transitive requirements of x/net.

Verified with the same settings the workflow uses:

  trivy fs --scanners vuln --severity CRITICAL,HIGH,MEDIUM \
    --ignore-unfixed --exit-code 1 .

which now exits 0. go build ./... and go test ./... pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-08-15 13:43:07 +08:00
dependabot[bot]
00cfe5aac2
chore(deps): bump the actions group across 1 directory with 4 updates (#4787)
Bumps the actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go), [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `actions/setup-go` from 6 to 7
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 13:42:11 +08:00
takanuva15
be4fe9b793 feat(binding): use encoding.UnmarshalText by default via build tag 2026-05-18 17:25:07 -04:00
Varun Chawla
019c54bbe6 feat(binding): auto-bind types implementing encoding.TextUnmarshaler
Types like uuid.UUID implement encoding.TextUnmarshaler but previously
required an explicit `parser=encoding.TextUnmarshaler` struct tag to be
used during form/query/URI binding. This meant ShouldBind/ShouldBindQuery
would fail with unhelpful errors like:
  ["45e1f85e-bca5-458d-bd9c-c56edd8f847b"] is not valid value for uuid.UUID

This change makes trySetCustom automatically check for
encoding.TextUnmarshaler as a fallback after BindUnmarshaler,
so any type implementing TextUnmarshaler (uuid.UUID, net.IP,
custom types, etc.) works out of the box with no extra tags.

Precedence order:
1. BindUnmarshaler (UnmarshalParam)
2. encoding.TextUnmarshaler (UnmarshalText)
3. Default type-based binding

time.Time is explicitly excluded from automatic TextUnmarshaler
handling since gin has dedicated time parsing with time_format,
time_utc, and time_location struct tags.

Fixes #2423

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
2026-05-18 17:19:09 -04:00
18 changed files with 562 additions and 56 deletions

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Setup golangci-lint
@ -41,6 +41,7 @@ jobs:
'--ldflags="-checklinkname=0" -tags sonic',
"-tags go_json",
"-race",
"-tags gin_bind_default_encoding_text_unmarshaler",
]
include:
- os: ubuntu-latest
@ -55,17 +56,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.ref }}
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: |
${{ matrix.go-build }}
@ -78,6 +79,6 @@ jobs:
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}

View File

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Run GoReleaser

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

@ -15,6 +15,7 @@ import (
"strings"
"time"
bindingcodec "github.com/gin-gonic/gin/codec/binding"
"github.com/gin-gonic/gin/codec/json"
"github.com/gin-gonic/gin/internal/bytesconv"
)
@ -185,17 +186,6 @@ type BindUnmarshaler interface {
UnmarshalParam(param string) error
}
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
case BindUnmarshaler:
return true, v.UnmarshalParam(val)
}
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) {
@ -265,7 +255,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
} else if ok, err = bindingcodec.API.TrySetByInterface(vs[0], value); ok {
return ok, err
}
@ -290,7 +280,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
} else if ok, err = bindingcodec.API.TrySetByInterface(vs[0], value); ok {
return ok, err
}
@ -313,7 +303,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(val, value); ok {
} else if ok, err = bindingcodec.API.TrySetByInterface(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field, opt)
@ -324,7 +314,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
// 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 {
} else if ok, err = bindingcodec.API.TrySetByInterface(val, value); ok {
return err
}

View File

@ -0,0 +1,31 @@
//go:build !gin_bind_default_encoding_text_unmarshaler
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// If someone does not specify parser=encoding.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)
//
// Note: TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined works differently when:
// - form_mapping_encoding_test.go (with gin_bind_default_encoding_text_unmarshaler build tag enabled)
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
}

View File

@ -0,0 +1,121 @@
//go:build gin_bind_default_encoding_text_unmarshaler
package binding
import (
"encoding"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// In gin_bind_default_encoding_text_unmarshaler mode, TextUnmarshaler is used automatically when present, even without an
// explicit parser tag.
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined_DefaultEncodingUnmarshalText(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, 0x11, s.Hex)
assert.EqualValues(t, 0x11, s.HexByUnmarshalText)
}
// ==== Automatic TextUnmarshaler binding tests (no parser tag required) ====
func TestMappingTextUnmarshalerAutoBindForm(t *testing.T) {
var s struct {
ID objectIDUnmarshalText `form:"id"`
}
err := mappingByPtr(&s, formSource{"id": {"664a062ac74a8ad104e0e80f"}}, "form")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80f")
assert.Equal(t, expected, s.ID)
}
func TestMappingTextUnmarshalerAutoBindURI(t *testing.T) {
var s struct {
ID objectIDUnmarshalText `uri:"id"`
}
err := mappingByPtr(&s, formSource{"id": {"664a062ac74a8ad104e0e80f"}}, "uri")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80f")
assert.Equal(t, expected, s.ID)
}
func TestMappingTextUnmarshalerAutoBindSlice(t *testing.T) {
var s struct {
IDs []objectIDUnmarshalText `form:"ids" collection_format:"csv"`
}
err := mappingByPtr(&s, formSource{"ids": {"664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f"}}, "form")
require.NoError(t, err)
id1, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80e")
id2, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80f")
expected := []objectIDUnmarshalText{id1, id2}
assert.Equal(t, expected, s.IDs)
}
func TestMappingTextUnmarshalerAutoBindMultipleValues(t *testing.T) {
var s struct {
IDs []objectIDUnmarshalText `form:"ids"`
}
err := mappingByPtr(&s, formSource{"ids": {
"664a062ac74a8ad104e0e80e",
"664a062ac74a8ad104e0e80f",
}}, "form")
require.NoError(t, err)
id1, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80e")
id2, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80f")
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.IDs)
}
func TestMappingTextUnmarshalerAutoBindDefault(t *testing.T) {
var s struct {
ID objectIDUnmarshalText `form:"id,default=664a062ac74a8ad104e0e80f"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
expected, _ := convertToOidUnmarshalText("664a062ac74a8ad104e0e80f")
assert.Equal(t, expected, s.ID)
}
func TestMappingTextUnmarshalerAutoBindInvalidValue(t *testing.T) {
var s struct {
ID objectIDUnmarshalText `form:"id"`
}
err := mappingByPtr(&s, formSource{"id": {"not-a-valid-objectid"}}, "form")
require.Error(t, err)
}
// BindUnmarshaler should take precedence over TextUnmarshaler
type testDualUnmarshaler struct {
Value string
}
func (d *testDualUnmarshaler) UnmarshalParam(param string) error {
d.Value = "param:" + param
return nil
}
func (d *testDualUnmarshaler) UnmarshalText(text []byte) error {
d.Value = "text:" + string(text)
return nil
}
var _ BindUnmarshaler = (*testDualUnmarshaler)(nil)
var _ encoding.TextUnmarshaler = (*testDualUnmarshaler)(nil)
func TestMappingBindUnmarshalerTakesPrecedenceOverTextUnmarshaler(t *testing.T) {
var s struct {
Field testDualUnmarshaler `form:"field"`
}
err := mappingByPtr(&s, formSource{"field": {"hello"}}, "form")
require.NoError(t, err)
assert.Equal(t, "param:hello", s.Field.Value) // BindUnmarshaler wins
}

View File

@ -1016,24 +1016,6 @@ func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDef
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 {

28
codec/binding/api.go Normal file
View File

@ -0,0 +1,28 @@
package bindingcodec
import "reflect"
func init() {
API = bindingApi{}
}
type bindingApi struct{}
// API the binding codec in use
var API Core
// Core the api for binding codec
type Core interface {
// TrySetByInterface tries to set valueToSet from inputVal using one of the optional interfaces that gin supports
//
// Returns:
// - isSet: whether the value was set successfully
// - err: any error that occurred during the setting process
TrySetByInterface(inputVal string, valueToSet reflect.Value) (isSet bool, err error)
}
// bindUnmarshaler duplicates binding.BindUnmarshaler to avoid an import cycle
// This must match binding.BindUnmarshaler exactly to maintain consistent behavior
type bindUnmarshaler interface {
UnmarshalParam(param string) error
}

12
codec/binding/api_test.go Normal file
View File

@ -0,0 +1,12 @@
package bindingcodec
import (
"testing"
)
// TestAPIInitialization tests that the API is properly initialized
func TestAPIInitialization(t *testing.T) {
if API == nil {
t.Fatal("API should not be nil after initialization")
}
}

20
codec/binding/default.go Normal file
View File

@ -0,0 +1,20 @@
//go:build !gin_bind_default_encoding_text_unmarshaler
package bindingcodec
import (
"reflect"
)
// Description summarizes what this binding codec does. This variable helps
// ensure that build tags are configured to be mutually exclusive
const Description = "Gin default binding api"
// TrySetByInterface uses bindUnmarshaler if implemented, otherwise returns false to revert to gin's default binding logic
func (d bindingApi) TrySetByInterface(inputVal string, valueToSet reflect.Value) (isSet bool, err error) {
switch v := valueToSet.Addr().Interface().(type) {
case bindUnmarshaler:
return true, v.UnmarshalParam(inputVal)
}
return false, nil
}

View File

@ -0,0 +1,89 @@
//go:build !gin_bind_default_encoding_text_unmarshaler
package bindingcodec
import (
"errors"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
// mockBindUnmarshaler implements the bindUnmarshaler interface for testing
type mockBindUnmarshaler struct {
receivedParam string
returnError error
}
func (m *mockBindUnmarshaler) UnmarshalParam(param string) error {
m.receivedParam = param
return m.returnError
}
// TestTrySetByInterface_WithBindUnmarshaler tests successful binding with bindUnmarshaler
func TestTrySetByInterface_WithBindUnmarshaler(t *testing.T) {
api := bindingApi{}
mock := &mockBindUnmarshaler{}
value := reflect.ValueOf(mock).Elem()
inputVal := "test-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.NoError(t, err)
require.Equal(t, "test-value", mock.receivedParam)
}
// TestTrySetByInterface_WithBindUnmarshalerError tests error handling from bindUnmarshaler
func TestTrySetByInterface_WithBindUnmarshalerError(t *testing.T) {
api := bindingApi{}
expectedErr := errors.New("unmarshal error")
mock := &mockBindUnmarshaler{returnError: expectedErr}
value := reflect.ValueOf(mock).Elem()
inputVal := "test-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.Error(t, err)
}
// TestTrySetByInterface_WithoutInterface tests behavior with regular types
func TestTrySetByInterface_WithoutInterface(t *testing.T) {
api := bindingApi{}
testCases := []struct {
name string
value any
}{
{"string", "test"},
{"int", 42},
{"bool", true},
{"struct", struct{ Field string }{"value"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a pointer to make it addressable
ptr := reflect.New(reflect.TypeOf(tc.value))
ptr.Elem().Set(reflect.ValueOf(tc.value))
value := ptr.Elem()
isSet, err := api.TrySetByInterface("input", value)
require.False(t, isSet)
require.NoError(t, err)
})
}
}
// TestTrySetByInterface_WithPointer tests that the method works with pointer values
func TestTrySetByInterface_WithPointer(t *testing.T) {
api := bindingApi{}
mock := &mockBindUnmarshaler{}
value := reflect.ValueOf(mock).Elem()
inputVal := "pointer-test"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.NoError(t, err)
require.Equal(t, inputVal, mock.receivedParam)
}

37
codec/binding/encoding.go Normal file
View File

@ -0,0 +1,37 @@
//go:build gin_bind_default_encoding_text_unmarshaler
package bindingcodec
import (
"encoding"
"log"
"reflect"
"time"
)
// Description summarizes what this binding codec does. This variable helps
// ensure that build tags are configured to be mutually exclusive
const Description = "Gin binding api using encoding.TextUnmarshaler"
func init() {
log.Println("[GIN-debug] gin_bind_default_encoding_text_unmarshaler flag active. Gin will bind using encoding.TextUnmarshaler by default")
}
// TrySetByInterface checks for bindUnmarshaler first, then falls back to encoding.TextUnmarshaler.
// This allows types which implement TextUnmarshaler (like uuid.UUID) to be bound
// automatically without requiring an explicit parser tag.
//
// Note: time.Time is excluded from automatic TextUnmarshaler handling because gin
// provides dedicated time parsing via time_format, time_utc, and time_location tags.
func (d bindingApi) TrySetByInterface(inputVal string, valueToSet reflect.Value) (isSet bool, err error) {
switch v := valueToSet.Addr().Interface().(type) {
case bindUnmarshaler:
return true, v.UnmarshalParam(inputVal)
case encoding.TextUnmarshaler:
// Skip time.Time — it has dedicated handling in setWithProperType via setTimeField
if _, isTime := valueToSet.Interface().(time.Time); !isTime {
return true, v.UnmarshalText([]byte(inputVal))
}
}
return false, nil
}

View File

@ -0,0 +1,152 @@
//go:build gin_bind_default_encoding_text_unmarshaler
package bindingcodec
import (
"encoding"
"errors"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// mockBindUnmarshaler implements the bindUnmarshaler interface for testing
type mockBindUnmarshaler struct {
receivedParam string
returnError error
}
func (m *mockBindUnmarshaler) UnmarshalParam(param string) error {
m.receivedParam = param
return m.returnError
}
// mockTextUnmarshaler implements encoding.TextUnmarshaler for testing
type mockTextUnmarshaler struct {
receivedText []byte
returnError error
}
func (m *mockTextUnmarshaler) UnmarshalText(text []byte) error {
m.receivedText = text
return m.returnError
}
func TestTrySetByInterface_WithBindUnmarshaler(t *testing.T) {
api := bindingApi{}
mock := &mockBindUnmarshaler{}
value := reflect.ValueOf(mock).Elem()
inputVal := "test-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.NoError(t, err)
require.Equal(t, inputVal, mock.receivedParam)
}
func TestTrySetByInterface_WithBindUnmarshalerError(t *testing.T) {
api := bindingApi{}
expectedErr := errors.New("unmarshal error")
mock := &mockBindUnmarshaler{returnError: expectedErr}
value := reflect.ValueOf(mock).Elem()
inputVal := "test-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.Error(t, err)
}
func TestTrySetByInterface_WithTextUnmarshaler(t *testing.T) {
api := bindingApi{}
mock := &mockTextUnmarshaler{}
value := reflect.ValueOf(mock).Elem()
inputVal := "text-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.NoError(t, err)
require.Equal(t, []byte(inputVal), mock.receivedText)
}
func TestTrySetByInterface_WithTextUnmarshalerError(t *testing.T) {
api := bindingApi{}
expectedErr := errors.New("text unmarshal error")
mock := &mockTextUnmarshaler{returnError: expectedErr}
value := reflect.ValueOf(mock).Elem()
inputVal := "text-value"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.Error(t, err)
}
func TestTrySetByInterface_WithTimeTime(t *testing.T) {
api := bindingApi{}
now := time.Now()
value := reflect.ValueOf(&now).Elem()
inputVal := "2023-01-01T00:00:00Z"
isSet, err := api.TrySetByInterface(inputVal, value)
require.False(t, isSet)
require.NoError(t, err)
}
// TestTrySetByInterface_WithoutInterface tests behavior with regular types
func TestTrySetByInterface_WithoutInterface(t *testing.T) {
api := bindingApi{}
testCases := []struct {
name string
value any
}{
{"string", "test"},
{"int", 42},
{"bool", true},
{"struct", struct{ Field string }{"value"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ptr := reflect.New(reflect.TypeOf(tc.value))
ptr.Elem().Set(reflect.ValueOf(tc.value))
value := ptr.Elem()
isSet, err := api.TrySetByInterface("input", value)
require.False(t, isSet)
require.NoError(t, err)
})
}
}
// mockBothInterfaces implements both bindUnmarshaler and encoding.TextUnmarshaler
type mockBothInterfaces struct {
bindCalled bool
textCalled bool
}
func (m *mockBothInterfaces) UnmarshalParam(param string) error {
m.bindCalled = true
return nil
}
func (m *mockBothInterfaces) UnmarshalText(text []byte) error {
m.textCalled = true
return nil
}
var _ encoding.TextUnmarshaler = (*mockBothInterfaces)(nil)
func TestTrySetByInterface_PriorityBindUnmarshaler(t *testing.T) {
api := bindingApi{}
mock := &mockBothInterfaces{}
value := reflect.ValueOf(mock).Elem()
inputVal := "test"
isSet, err := api.TrySetByInterface(inputVal, value)
require.True(t, isSet)
require.NoError(t, err)
require.True(t, mock.bindCalled)
require.False(t, mock.textCalled)
}

View File

@ -39,6 +39,7 @@
- [Bind default value if none provided](#bind-default-value-if-none-provided)
- [Collection format for arrays](#collection-format-for-arrays)
- [Bind Uri](#bind-uri)
- [Bind with encoding.TextUnmarshaler by default](#bind-with-encodingTextUnmarshaler-by-default)
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes)
@ -1228,6 +1229,48 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
curl -v localhost:8088/thinkerou/not-uuid
```
### Bind with encoding.TextUnmarshaler by default
By default, Gin binds using its own logic that does not account for `encoding.TextUnmarshaler`.
To bind using `encoding.TextUnmarshaler` by default, add the build tag `gin_bind_default_encoding_text_unmarshaler` during the build process.
```go
package main
import (
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
)
func main() {
route := gin.Default()
var request struct {
Uuid uuid.UUID `form:"uuid" binding:"uuid"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request)
})
_ = route.Run(":8088")
}
```
Test it with:
```sh
go run -tags=gin_bind_default_encoding_text_unmarshaler main.go
# Then in another terminal
curl 'localhost:8088/test?uuid=70d9b500-fa26-11dd-876f-322495cdf0f7'
```
Result
```sh
{"Uuid":"70d9b500-fa26-11dd-876f-322495cdf0f7"}
```
Note:
- This feature is not active by default to avoid breaking changes for existing users dependent on gin's default binding logic
### 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.

8
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.55.0
golang.org/x/net v0.56.0
google.golang.org/protobuf v1.36.11
)
@ -39,7 +39,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)

16
go.sum
View File

@ -79,15 +79,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -55,7 +55,7 @@ func cleanPath(p string) string {
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n {