Repository: gin-gonic/gin
Pull request #4193 (2025-03-20 09:53:23)

fix(binding): prevent duplicate decoding and add validation in decodeToml
```go
// binding/toml.go:29
func decodeToml(r io.Reader, obj any) error {
	decoder := toml.NewDecoder(r)
	if err := decoder.Decode(obj); err != nil {
		return err
	}
	return decoder.Decode(obj)
}
```
- Removed the redundant second call to `decoder.Decode(obj)` which caused incorrect behavior.
- Added a `validate(obj)` function to ensure proper validation of decoded TOML data.
- Fixes potential issues with data integrity caused by duplicate decoding.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gin-gonic/gin/pull/4193?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.99%. Comparing base [(`3dc1cd6`)](https://app.codecov.io/gh/gin-gonic/gin/commit/3dc1cd6572b4e3a0cd170a15debe546c2c72294f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) to head [(`0a36ef7`)](https://app.codecov.io/gh/gin-gonic/gin/commit/0a36ef76f2a1b4fb390cf52c291bf8bba0f25f63?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).
> Report is 93 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4193      +/-   ##
==========================================
- Coverage   99.21%   98.99%   -0.22%     
==========================================
  Files          42       44       +2     
  Lines        3182     3395     +213     
==========================================
+ Hits         3157     3361     +204     
- Misses         17       24       +7     
- Partials        8       10       +2     
```

| [Flag](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | Coverage Δ | |
|---|---|---|
| [](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [--ldflags="-checklinkname=0" -tags "sonic avx"](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.99% <100.00%> (?)` | |
| [-tags nomsgpack](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.98% <100.00%> (?)` | |
| [go-1.18](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.19](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.20](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.21](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.23](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.99% <100.00%> (?)` | |
| [macos-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.99% <100.00%> (-0.23%)` | :arrow_down: |
| [ubuntu-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/4193/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.98% <100.00%> (-0.23%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gin-gonic/gin/pull/4193?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index f5f6f3a..235692d 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -398,18 +398,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
 	}
 
 	switch tf := strings.ToLower(timeFormat); tf {
-	case "unix", "unixnano":
+	case "unix", "unixmilli", "unixmicro", "unixnano":
 		tv, err := strconv.ParseInt(val, 10, 64)
 		if err != nil {
 			return err
 		}
 
-		d := time.Duration(1)
-		if tf == "unixnano" {
-			d = time.Second
+		var t time.Time
+		switch tf {
+		case "unix":
+			t = time.Unix(tv, 0)
+		case "unixmilli":
+			t = time.UnixMilli(tv)
+		case "unixmicro":
+			t = time.UnixMicro(tv)
+		default:
+			t = time.Unix(0, tv)
 		}
 
-		t := time.Unix(tv/int64(d), tv%int64(d))
 		value.Set(reflect.ValueOf(t))
 		return nil
 	}
diff --git a/binding/toml.go b/binding/toml.go
index a66b93a..2681231 100644
--- a/binding/toml.go
+++ b/binding/toml.go
@@ -31,5 +31,5 @@ func decodeToml(r io.Reader, obj any) error {
 	if err := decoder.Decode(obj); err != nil {
 		return err
 	}
-	return decoder.Decode(obj)
+	return validate(obj)
 }
diff --git a/docs/doc.md b/docs/doc.md
index a463e82..bea417b 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -832,6 +832,8 @@ type Person struct {
   Birthday   time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
   CreateTime time.Time `form:"createTime" time_format:"unixNano"`
   UnixTime   time.Time `form:"unixTime" time_format:"unix"`
+  UnixMilliTime   time.Time `form:"unixMilliTime" time_format:"unixmilli"`
+  UnixMicroTime   time.Time `form:"unixMicroTime" time_format:"uNiXmIcRo"` // case does not matter for "unix*" time formats
 }
 
 func main() {
@@ -851,6 +853,8 @@ func startPage(c *gin.Context) {
     log.Println(person.Birthday)
     log.Println(person.CreateTime)
     log.Println(person.UnixTime)
+    log.Println(person.UnixMilliTime)
+    log.Println(person.UnixMicroTime)
   }
 
   c.String(http.StatusOK, "Success")
@@ -860,7 +864,7 @@ func startPage(c *gin.Context) {
 Test it with:
 
 ```sh
-curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
+curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012"
 ```
 
 


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 901e974..bdab369 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -69,15 +69,19 @@ type FooStructDisallowUnknownFields struct {
 }
 
 type FooBarStructForTimeType struct {
-	TimeFoo    time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
-	TimeBar    time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
-	CreateTime time.Time `form:"createTime" time_format:"unixNano"`
-	UnixTime   time.Time `form:"unixTime" time_format:"unix"`
+	TimeFoo       time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
+	TimeBar       time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
+	CreateTime    time.Time `form:"createTime" time_format:"unixNano"`
+	UnixTime      time.Time `form:"unixTime" time_format:"unix"`
+	UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixmilli"`
+	UnixMicroTime time.Time `form:"unixMicroTime" time_format:"uNiXmiCrO"`
 }
 
 type FooStructForTimeTypeNotUnixFormat struct {
-	CreateTime time.Time `form:"createTime" time_format:"unixNano"`
-	UnixTime   time.Time `form:"unixTime" time_format:"unix"`
+	CreateTime    time.Time `form:"createTime" time_format:"unixNano"`
+	UnixTime      time.Time `form:"unixTime" time_format:"unix"`
+	UnixMilliTime time.Time `form:"unixMilliTime" time_format:"unixMilli"`
+	UnixMicroTime time.Time `form:"unixMicroTime" time_format:"unixMicro"`
 }
 
 type FooStructForTimeTypeNotFormat struct {
@@ -265,10 +269,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
 func TestBindingFormForTime(t *testing.T) {
 	testFormBindingForTime(t, http.MethodPost,
 		"/", "/",
-		"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
+		"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "bar2=foo")
 	testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
 		"/", "/",
-		"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
+		"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
 	testFormBindingForTimeNotFormat(t, http.MethodPost,
 		"/", "/",
 		"time_foo=2017-11-15", "bar2=foo")
@@ -282,11 +286,11 @@ func TestBindingFormForTime(t *testing.T) {
 
 func TestBindingFormForTime2(t *testing.T) {
 	testFormBindingForTime(t, http.MethodGet,
-		"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
+		"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033&unixMilliTime=1562400033001&unixMicroTime=1562400033000012", "/?bar2=foo",
 		"", "")
 	testFormBindingForTimeNotUnixFormat(t, http.MethodPost,
 		"/", "/",
-		"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
+		"time_foo=2017-11-15&createTime=bad&unixTime=bad&unixMilliTime=bad&unixMicroTime=bad", "bar2=foo")
 	testFormBindingForTimeNotFormat(t, http.MethodGet,
 		"/?time_foo=2017-11-15", "/?bar2=foo",
 		"", "")
@@ -952,6 +956,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
 	assert.Equal(t, "UTC", obj.TimeBar.Location().String())
 	assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
 	assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
+	assert.Equal(t, int64(1562400033001), obj.UnixMilliTime.UnixMilli())
+	assert.Equal(t, int64(1562400033000012), obj.UnixMicroTime.UnixMicro())
 
 	obj = FooBarStructForTimeType{}
 	req = requestWithBody(method, badPath, badBody)