Repository: gogf/gf

Issue:
请求参数如果是bool型，false会被解析为true,只有0才会解析为false
### Go version

arm64

### GoFrame version

ex 2.7

### Can this bug be reproduced with the latest release?

Option Yes

### What did you do?
```json
{
    "state": "",
    "pagination": false
} 
```
请求参数中的false会被解析为true,只有0才会false

### What did you see happen?
```json
{
    "state": "",
    "pagination": false
} 
```
请求参数中的false会被解析为true,只有0才会false

### What did you expect to see?
```json
{
    "state": "",
    "pagination": false
} 
```
请求参数中的false会被解析为true,只有0才会false

Discussion:
@wln32: 请给出完整示例代码@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
Please give complete sample code  
      @Q32611600: ```go
// PageReq 分页请求
type PageReq struct {
	Page       int  `json:"page" example:"10" d:"1" v:"min:1#页码最小值不能低于1"  dc:"当前页码"`
	PerPage    int  `json:"pageSize" example:"1" d:"10" v:"min:1|max:200#每页数量最小值不能低于1|最大值不能大于200" dc:"每页数量"`
	Pagination bool `json:"pagination" d:"true" dc:"是否需要进行分页"`
}
```
比如这里的是否分页
控制器里面用 if req.Pagination {
提交的参数是否false 也会走分页，只有传入0才会不走分页
}@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
// PageReq paging request
typePageReq struct {
Page int `json:"page" example:"10" d:"1" v:"min:1#The minimum value of the page number cannot be less than 1" dc:"Current page number"`
PerPage int `json:"pageSize" example:"1" d:"10" v:"min:1|max:200#The minimum value of the number per page cannot be less than 1 | the maximum value cannot be greater than 200" dc:"Per page Quantity"`
Pagination bool `json:"pagination" d:"true" dc:"Whether pagination is required"`
}
For example, here is whether to paginate
Use if req.Pagination { in the controller
Whether the submitted parameters are false will also cause paging. Only when 0 is passed in will pagination not occur.
}  
      @wln32: @Q32611600 我在2.7.0版本中尝试了没有问题，false会被正确解析为false@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
@Q32611600 I tried it in version 2.7.0 with no problem, false will be correctly parsed as false  
      @Q32611600: v2.8.0
Welcome to GoFrame!
Env Detail:
  Go Version: go1.23.1 windows/amd64
  GF Version(go.mod): 
    github.com/gogf/gf/contrib/drivers/mssql/v2@v2.8.1
    github.com/gogf/gf/contrib/nosql/redis/v2@v2.8.1
    github.com/gogf/gf/v2@v2.8.1
CLI Detail:
  Installed At: C:\Go\bin\gf.exe
  Built Go Version: go1.22.5
  Built GF Version: v2.8.0
  Git Commit: 2024-11-17 10:57:17 ec6c537defd5c0194de18e2baa9eb2e20bc12978
  Built Time: 2024-11-17 11:03:05
这是我当前用的版本@wln32: @Q32611600 看起来是字段的d tag的问题，我加了d tag问题就复现了@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
@Q32611600 It seems to be a problem with the d tag of the field. I added the d tag and the problem reappeared.  
      @Q32611600: 强仔
***@***.***



&nbsp;




------------------&nbsp;原始邮件&nbsp;------------------
发件人:                                                                                                                        "gogf/gf"                                                                                    ***@***.***&gt;;
发送时间:&nbsp;2025年1月5日(星期天) 晚上6:10
***@***.***&gt;;
***@***.******@***.***&gt;;
主题:&nbsp;Re: [gogf/gf] 请求参数如果是bool型，false会被解析为true,只有0才会解析为false (Issue #4093)





 
@Q32611600 我在2.7.0版本中尝试了没有问题，false会被正确解析为false
 
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***&gt;@Q32611600: 的确如此，我把缺省值去掉后测试。正如您所说是正确的




强仔
***@***.***



&nbsp;




------------------&nbsp;原始邮件&nbsp;------------------
发件人:                                                                                                                        "gogf/gf"                                                                                    ***@***.***&gt;;
发送时间:&nbsp;2025年1月5日(星期天) 晚上6:14
***@***.***&gt;;
***@***.******@***.***&gt;;
主题:&nbsp;Re: [gogf/gf] 请求参数如果是bool型，false会被解析为true,只有0才会解析为false (Issue #4093)





 
@Q32611600 看起来是字段的d tag的问题，我加了d tag问题就复现了
 
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***&gt;@wln32: @gqcn 目前mergeDefaultStructValue这个函数看起来有逻辑错误，当用户传值时，如果传的是类型的默认值，并且如果给字段设置了默认值的话(当和类型的默认值不同时)，会给这个字段设置默认值
![image](https://github.com/user-attachments/assets/e9edcea4-e0e1-47a5-a1cc-8ef6df7c875e)
@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
@gqcn At present, the mergeDefaultStructValue function seems to have a logical error. When the user passes a value, if the default value of the type is passed, and if a default value is set for the field (when it is different from the default value of the type), this will be given Field settings default value
![image](https://github.com/user-attachments/assets/e9edcea4-e0e1-47a5-a1cc-8ef6df7c875e)  
      @wln32: @gqcn 我尝试了数字类型和字符串类型添加默认值时，数字类型不会被设置默认值，因为它被解析为json.Number，传0时，会被认为非空的
![image](https://github.com/user-attachments/assets/2b2cc34d-52bc-4819-8a94-8643f0946d69)

使用以下参数传递
```json
{
"state": "",
"pagination": false,
"k1":0
} 
```

```go
// 对应的结构体
type HelloReq struct {
	g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
	State      string `json:"state"  d:"default_state"`
	Pagination bool   `json:"pagination" d:"true"`
	K1         int    `json:"k1" d:"198"`
}
```
@lzgzhyf: 请问这个Bool默认值问题有进展吗？
v2.8.3问题依旧存在@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
Is there any progress on this Bool default value issue?
v2.8.3 problem still exists  
      @hailaz: 补充一下复现必须条件

- post请求
- json body传参
- 字段类型为bool
- 字段设置了`default`tag

关键点，传参字段的类型被识别为bool且值为`false`，会被认为是空值，然后覆盖为默认值

需要讨论如何处理，并同步修复多处相同逻辑的代码。

代码定位如下
https://github.com/gogf/gf/blob/63cb3285f8311c14e6354c1a3aeb7ab103f26ad2/net/ghttp/ghttp_request_param_request.go#L207@hailaz: 补充资料
https://goframe.org/docs/web/request-default-value#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
![Image](https://github.com/user-attachments/assets/46486bbb-512c-4efe-9857-eb45461d1416)

需要区分出来哪些是有效参数，哪些是无效参数

例如k1=v1&k2这样的，k2算不算有效的@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
Supplementary information
https://goframe.org/docs/web/request-default-value#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
![Image](https://github.com/user-attachments/assets/46486bbb-512c-4efe-9857-eb45461d1416)

It is necessary to distinguish which are valid parameters and which are invalid parameters

For example, if k1=v1&k2 is like this, is k2 considered valid?  
      @wln32: > 补充资料 https://goframe.org/docs/web/request-default-value#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9 ![Image](https://github.com/user-attachments/assets/46486bbb-512c-4efe-9857-eb45461d1416)
> 
> 需要区分出来哪些是有效参数，哪些是无效参数
> 
> 例如k1=v1&k2这样的，k2算不算有效的

@hailaz http对于参数的解析，依赖gstr.Parse函数，我测试了一下，结果如下
```go
m1, _ := gstr.Parse("k1=v1&k2")
// output: map[k1:v1]
m2, _ := gstr.Parse("k1=v1&k2=")
// output: map[k1:v1 k2:]
```
当key有=号时，它被解析为有效的参数，没有=号时，视为无效的，文档可以增加一下说明

@Issues-translate-bot:  
> Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿         
----    
> Supplementary information https://goframe.org/docs/web/request-default-value#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9 ![Image](https://github.com/user-attachments/assets/46486bbb-512c-4efe-9857-eb45461d1416)
>
> It is necessary to distinguish which are valid parameters and which are invalid parameters
>
> For example, if k1=v1&k2 is like this, is k2 considered valid?

@hailaz http For parameter parsing, I rely on the gstr.Parse function. I tested it and the result is as follows
```go
m1, _ := gstr.Parse("k1=v1&k2")
// output: map[k1:v1]
m2, _ := gstr.Parse("k1=v1&k2=")
// output: map[k1:v1 k2:]
```
When the key has an = sign, it is parsed as a valid parameter. If there is no = sign, it is considered invalid. The document can add a description.  
      @wln32: > 补充一下复现必须条件
> 
> * post请求
> * json body传参
> * 字段类型为bool
> * 字段设置了`default`tag
> 
> 关键点，传参字段的类型被识别为bool且值为`false`，会被认为是空值，然后覆盖为默认值
> 
> 需要讨论如何处理，并同步修复多处相同逻辑的代码。
> 
> 代码定位如下
> 
> [gf/net/ghttp/ghttp_request_param_request.go](https://github.com/gogf/gf/blob/63cb3285f8311c14e6354c1a3aeb7ab103f26ad2/net/ghttp/ghttp_request_param_request.go#L207)
> 
> Line 207 in [63cb328](/gogf/gf/commit/63cb3285f8311c14e6354c1a3aeb7ab103f26ad2)
> 
>  if empty.IsEmpty(foundValue) {

@hailaz 目前ghttp解析json body时，使用的是标准库的json.Unmarshal,当它解析到map时，和go的类型对应关系如下
Number => json.Number   实际定义type Number string
String     => string
null        => nil
Bool       => bool
Object   => map[string]any
Array     => []any

根据原有的逻辑来看，当前端传的key的类型为string或者bool的默认值时，应该都会被认为没有传值才对，

```go
type HelloReq struct {
	g.Meta     `path:"/hello" tags:"Hello" method:"post" summary:"You first hello api"`
	Page       int    `json:"page" example:"10" d:"1" v:"min:1#页码最小值不能低于1"  dc:"当前页码"`
	PerPage    int    `json:"pageSize" example:"1" d:"10" v:"min:1|max:200#每页数量最小值不能低于1|最大值不能大于200" dc:"每页数量"`
	Pagination bool   `json:"pagination" d:"true" dc:"是否需要进行分页"`
	K1         string `json:"k1" d:"k1_default"`
}

func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
	g.Log().Info(ctx, req)
	g.RequestFromCtx(ctx).Response.WriteJson(req)
	return
}

```
#### 请求的json数据
```json
 {
    "state": "",
    "pagination": false,
    "k1":""
} 
```

#### 控制台输出结果
` {"page":1,"pageSize":10,"pagination":true,"k1":"k1_default"} `

## 可以看到bool类型和string似乎都被设置默认值了

Solution patch:
diff --git a/net/ghttp/ghttp_request_param.go b/net/ghttp/ghttp_request_param.go
index b7f7e67e4..6e44814c0 100644
--- a/net/ghttp/ghttp_request_param.go
+++ b/net/ghttp/ghttp_request_param.go
@@ -107,7 +107,6 @@ func (r *Request) doParse(pointer interface{}, requestType int) error {
 				return err
 			}
 		}
-		// TODO: https://github.com/gogf/gf/pull/2450
 		// Validation.
 		if err = gvalid.New().
 			Bail().
diff --git a/net/ghttp/ghttp_request_param_request.go b/net/ghttp/ghttp_request_param_request.go
index 601e3f06a..622590f09 100644
--- a/net/ghttp/ghttp_request_param_request.go
+++ b/net/ghttp/ghttp_request_param_request.go
@@ -8,7 +8,6 @@ package ghttp
 
 import (
 	"github.com/gogf/gf/v2/container/gvar"
-	"github.com/gogf/gf/v2/internal/empty"
 	"github.com/gogf/gf/v2/net/goai"
 	"github.com/gogf/gf/v2/os/gstructs"
 	"github.com/gogf/gf/v2/util/gconv"
@@ -178,15 +177,17 @@ func (r *Request) doGetRequestStruct(pointer interface{}, mapping ...map[string]
 	if data == nil {
 		data = map[string]interface{}{}
 	}
-	// Default struct values.
-	if err = r.mergeDefaultStructValue(data, pointer); err != nil {
-		return data, nil
-	}
+
 	// `in` Tag Struct values.
 	if err = r.mergeInTagStructValue(data); err != nil {
 		return data, nil
 	}
 
+	// Default struct values.
+	if err = r.mergeDefaultStructValue(data, pointer); err != nil {
+		return data, nil
+	}
+
 	return data, gconv.Struct(data, pointer, mapping...)
 }
 
@@ -194,20 +195,9 @@ func (r *Request) doGetRequestStruct(pointer interface{}, mapping ...map[string]
 func (r *Request) mergeDefaultStructValue(data map[string]interface{}, pointer interface{}) error {
 	fields := r.serveHandler.Handler.Info.ReqStructFields
 	if len(fields) > 0 {
-		var (
-			foundKey   string
-			foundValue interface{}
-		)
 		for _, field := range fields {
 			if tagValue := field.TagDefault(); tagValue != "" {
-				foundKey, foundValue = gutil.MapPossibleItemByKey(data, field.Name())
-				if foundKey == "" {
-					data[field.Name()] = tagValue
-				} else {
-					if empty.IsEmpty(foundValue) {
-						data[foundKey] = tagValue
-					}
-				}
+				mergeTagValueWithFoundKey(data, false, field.Name(), field.Name(), tagValue)
 			}
 		}
 		return nil
@@ -219,19 +209,8 @@ func (r *Request) mergeDefaultStructValue(data map[string]interface{}, pointer i
 		return err
 	}
 	if len(tagFields) > 0 {
-		var (
-			foundKey   string
-			foundValue interface{}
-		)
 		for _, field := range tagFields {
-			foundKey, foundValue = gutil.MapPossibleItemByKey(data, field.Name())
-			if foundKey == "" {
-				data[field.Name()] = field.TagValue
-			} else {
-				if empty.IsEmpty(foundValue) {
-					data[foundKey] = field.TagValue
-				}
-			}
+			mergeTagValueWithFoundKey(data, false, field.Name(), field.Name(), field.TagValue)
 		}
 	}
 
@@ -261,34 +240,29 @@ func (r *Request) mergeInTagStructValue(data map[string]interface{}) error {
 
 		for _, field := range fields {
 			if tagValue := field.TagIn(); tagValue != "" {
+				findKey := field.TagPriorityName()
 				switch tagValue {
 				case goai.ParameterInHeader:
-					foundHeaderKey, foundHeaderValue := gutil.MapPossibleItemByKey(headerMap, field.TagPriorityName())
-					if foundHeaderKey != "" {
-						foundKey, foundValue = gutil.MapPossibleItemByKey(data, foundHeaderKey)
-						if foundKey == "" {
-							data[field.Name()] = foundHeaderValue
-						} else {
-							if empty.IsEmpty(foundValue) {
-								data[foundKey] = foundHeaderValue
-							}
-						}
-					}
+					foundKey, foundValue = gutil.MapPossibleItemByKey(headerMap, findKey)
 				case goai.ParameterInCookie:
-					foundCookieKey, foundCookieValue := gutil.MapPossibleItemByKey(cookieMap, field.TagPriorityName())
-					if foundCookieKey != "" {
-						foundKey, foundValue = gutil.MapPossibleItemByKey(data, foundCookieKey)
-						if foundKey == "" {
-							data[field.Name()] = foundCookieValue
-						} else {
-							if empty.IsEmpty(foundValue) {
-								data[foundKey] = foundCookieValue
-							}
-						}
-					}
+					foundKey, foundValue = gutil.MapPossibleItemByKey(cookieMap, findKey)
+				}
+				if foundKey != "" {
+					mergeTagValueWithFoundKey(data, true, foundKey, field.Name(), foundValue)
 				}
 			}
 		}
 	}
 	return nil
 }
+
+// mergeTagValueWithFoundKey merges the request parameters when the key does not exist in the map or overwritten is true or the value is nil.
+func mergeTagValueWithFoundKey(data map[string]interface{}, overwritten bool, findKey string, fieldName string, tagValue interface{}) {
+	if foundKey, foundValue := gutil.MapPossibleItemByKey(data, findKey); foundKey == "" {
+		data[fieldName] = tagValue
+	} else {
+		if overwritten || foundValue == nil {
+			data[foundKey] = tagValue
+		}
+	}
+}
diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go
index 759e97239..cd1c40210 100644
--- a/util/gvalid/gvalid_validator_check_value.go
+++ b/util/gvalid/gvalid_validator_check_value.go
@@ -287,26 +287,40 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue
 		}
 
 	case reflect.Slice, reflect.Array:
-		var array []interface{}
-		if gjson.Valid(in.Value) {
-			array = gconv.Interfaces(gconv.Bytes(in.Value))
-		} else {
-			array = gconv.Interfaces(in.Value)
-		}
-		if len(array) == 0 {
-			return
+		loop := false
+		switch in.Type.Elem().Kind() {
+		// []struct []map
+		case reflect.Struct, reflect.Map:
+			loop = true
+		case reflect.Ptr:
+			loop = true
 		}
-		for _, item := range array {
-			v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
-				Value:               item,
-				Type:                in.Type.Elem(),
-				Kind:                in.Type.Elem().Kind(),
-				ErrorMaps:           in.ErrorMaps,
-				ResultSequenceRules: in.ResultSequenceRules,
-			})
-			// Bail feature.
-			if v.bail && len(in.ErrorMaps) > 0 {
-				break
+		// When it is a base type array,
+		// there is no need for recursive loop validation,
+		// otherwise it will cause memory leakage
+		// https://github.com/gogf/gf/issues/4092
+		if loop {
+			var array []interface{}
+			if gjson.Valid(in.Value) {
+				array = gconv.Interfaces(gconv.Bytes(in.Value))
+			} else {
+				array = gconv.Interfaces(in.Value)
+			}
+			if len(array) == 0 {
+				return
+			}
+			for _, item := range array {
+				v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
+					Value:               item,
+					Type:                in.Type.Elem(),
+					Kind:                in.Type.Elem().Kind(),
+					ErrorMaps:           in.ErrorMaps,
+					ResultSequenceRules: in.ResultSequenceRules,
+				})
+				// Bail feature.
+				if v.bail && len(in.ErrorMaps) > 0 {
+					break
+				}
 			}
 		}
 	}



Test patch:
diff --git a/net/ghttp/ghttp_z_unit_feature_request_param_test.go b/net/ghttp/ghttp_z_unit_feature_request_param_test.go
index b8d777673..3b9000c74 100644
--- a/net/ghttp/ghttp_z_unit_feature_request_param_test.go
+++ b/net/ghttp/ghttp_z_unit_feature_request_param_test.go
@@ -13,34 +13,34 @@ import (
 	"github.com/gogf/gf/v2/util/guid"
 )
 
-type UserReq struct {
+// UserTagInReq struct tag "in" supports: header, cookie
+type UserTagInReq struct {
 	g.Meta `path:"/user" tags:"User" method:"post" summary:"user api" title:"api title"`
 	Id     int    `v:"required" d:"1"`
 	Name   string `v:"required" in:"cookie"`
 	Age    string `v:"required" in:"header"`
-	// header,query,cookie,form
 }
 
-type UserRes struct {
+type UserTagInRes struct {
 	g.Meta `mime:"text/html" example:"string"`
 }
 
 var (
-	User = cUser{}
+	UserTagIn = cUserTagIn{}
 )
 
-type cUser struct{}
+type cUserTagIn struct{}
 
-func (c *cUser) User(ctx context.Context, req *UserReq) (res *UserRes, err error) {
+func (c *cUserTagIn) User(ctx context.Context, req *UserTagInReq) (res *UserTagInRes, err error) {
 	g.RequestFromCtx(ctx).Response.WriteJson(req)
 	return
 }
 
-func Test_Params_Tag(t *testing.T) {
+func Test_ParamsTagIn(t *testing.T) {
 	s := g.Server(guid.S())
 	s.Group("/", func(group *ghttp.RouterGroup) {
 		group.Middleware(ghttp.MiddlewareHandlerResponse)
-		group.Bind(User)
+		group.Bind(UserTagIn)
 	})
 	s.SetDumpRouterMap(false)
 	s.Start()
@@ -56,17 +56,101 @@ func Test_Params_Tag(t *testing.T) {
 		client.SetHeader("age", "18")
 
 		t.Assert(client.PostContent(ctx, "/user"), `{"Id":1,"Name":"john","Age":"18"}`)
-		t.Assert(client.PostContent(ctx, "/user", "name=&age=&id="), `{"Id":1,"Name":"john","Age":"18"}`)
+		t.Assert(client.PostContent(ctx, "/user", "name=&age="), `{"Id":1,"Name":"john","Age":"18"}`)
 	})
 }
 
-func Benchmark_ParamTag(b *testing.B) {
+type UserTagDefaultReq struct {
+	g.Meta   `path:"/user-default" method:"post,get" summary:"user default tag api"`
+	Id       int     `v:"required" d:"1"`
+	Name     string  `d:"john"`
+	Age      int     `d:"18"`
+	Score    float64 `d:"99.9"`
+	IsVip    bool    `d:"true"`
+	NickName string  `p:"nickname" d:"nickname-default"`
+	EmptyStr string  `d:""`
+	Email    string
+	Address  string
+}
+
+type UserTagDefaultRes struct {
+	g.Meta `mime:"application/json" example:"string"`
+}
+
+var (
+	UserTagDefault = cUserTagDefault{}
+)
+
+type cUserTagDefault struct{}
+
+func (c *cUserTagDefault) User(ctx context.Context, req *UserTagDefaultReq) (res *UserTagDefaultRes, err error) {
+	g.RequestFromCtx(ctx).Response.WriteJson(req)
+	return
+}
+
+func Test_ParamsTagDefault(t *testing.T) {
+	s := g.Server(guid.S())
+	s.Group("/", func(group *ghttp.RouterGroup) {
+		group.Middleware(ghttp.MiddlewareHandlerResponse)
+		group.Bind(UserTagDefault)
+	})
+	s.SetDumpRouterMap(false)
+	s.Start()
+	defer s.Shutdown()
+
+	time.Sleep(100 * time.Millisecond)
+
+	gtest.C(t, func(t *gtest.T) {
+		prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())
+		client := g.Client()
+		client.SetPrefix(prefix)
+
+		// Test with no parameters, should use all default values
+		resp := client.GetContent(ctx, "/user-default")
+		t.Assert(resp, `{"Id":1,"Name":"john","Age":18,"Score":99.9,"IsVip":true,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test with partial parameters (query method), should use partial default values
+		resp = client.GetContent(ctx, "/user-default?id=100&name=smith")
+		t.Assert(resp, `{"Id":100,"Name":"smith","Age":18,"Score":99.9,"IsVip":true,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test with partial parameters (query method), should use partial default values
+		resp = client.GetContent(ctx, "/user-default?id=100&name=smith&age")
+		t.Assert(resp, `{"Id":100,"Name":"smith","Age":18,"Score":99.9,"IsVip":true,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test providing partial parameters via POST form
+		resp = client.PostContent(ctx, "/user-default", "id=200&age=30&nickname=jack")
+		t.Assert(resp, `{"Id":200,"Name":"john","Age":30,"Score":99.9,"IsVip":true,"NickName":"jack","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test providing partial parameters via POST JSON
+		resp = client.ContentJson().PostContent(ctx, "/user-default", g.Map{
+			"id":      300,
+			"name":    "bob",
+			"score":   88.8,
+			"address": "beijing",
+		})
+		t.Assert(resp, `{"Id":300,"Name":"bob","Age":18,"Score":88.8,"IsVip":true,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":"beijing"}`)
+
+		// Test providing JSON content via GET request
+		resp = client.ContentJson().PostContent(ctx, "/user-default", `{"id":500,"isVip":false}`)
+		t.Assert(resp, `{"Id":500,"Name":"john","Age":18,"Score":99.9,"IsVip":false,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test providing empty values, should use default values
+		resp = client.PostContent(ctx, "/user-default", "id=400&name=&age=")
+		t.Assert(resp, `{"Id":400,"Name":"","Age":0,"Score":99.9,"IsVip":true,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+
+		// Test providing JSON content via GET request
+		resp = client.ContentJson().GetContent(ctx, "/user-default", `{"id":500,"isVip":false}`)
+		t.Assert(resp, `{"Id":500,"Name":"john","Age":18,"Score":99.9,"IsVip":false,"NickName":"nickname-default","EmptyStr":"","Email":"","Address":""}`)
+	})
+}
+
+func Benchmark_ParamTagIn(b *testing.B) {
 	b.StopTimer()
 
 	s := g.Server(guid.S())
 	s.Group("/", func(group *ghttp.RouterGroup) {
 		group.Middleware(ghttp.MiddlewareHandlerResponse)
-		group.Bind(User)
+		group.Bind(UserTagIn)
 	})
 	s.SetDumpRouterMap(false)
 	s.SetAccessLogEnabled(false)
diff --git a/net/ghttp/ghttp_z_unit_issue_test.go b/net/ghttp/ghttp_z_unit_issue_test.go
index df5838301..92f761ce9 100644
--- a/net/ghttp/ghttp_z_unit_issue_test.go
+++ b/net/ghttp/ghttp_z_unit_issue_test.go
@@ -678,3 +678,51 @@ func Test_Issue4047(t *testing.T) {
 		t.Assert(s.Logger(), nil)
 	})
 }
+
+// Issue4093Req
+type Issue4093Req struct {
+	g.Meta     `path:"/test" method:"post"`
+	Page       int    `json:"page" example:"10" d:"1" v:"min:1#页码最小值不能低于1"  dc:"当前页码"`
+	PerPage    int    `json:"pageSize" example:"1" d:"10" v:"min:1|max:200#每页数量最小值不能低于1|最大值不能大于200" dc:"每页数量"`
+	Pagination bool   `json:"pagination" d:"true" dc:"是否需要进行分页"`
+	Name       string `json:"name" d:"john"`
+	Number     int    `json:"number" d:"1"`
+}
+
+type Issue4093Res struct {
+	g.Meta `mime:"text/html" example:"string"`
+}
+
+var (
+	Issue4093 = cIssue4093{}
+)
+
+type cIssue4093 struct{}
+
+func (c *cIssue4093) User(ctx context.Context, req *Issue4093Req) (res *Issue4093Res, err error) {
+	g.RequestFromCtx(ctx).Response.WriteJson(req)
+	return
+}
+
+// https://github.com/gogf/gf/issues/4093
+func Test_Issue4093(t *testing.T) {
+	s := g.Server(guid.S())
+	s.Group("/", func(group *ghttp.RouterGroup) {
+		group.Middleware(ghttp.MiddlewareHandlerResponse)
+		group.Bind(Issue4093)
+	})
+	s.SetDumpRouterMap(false)
+	s.Start()
+	defer s.Shutdown()
+
+	time.Sleep(100 * time.Millisecond)
+
+	gtest.C(t, func(t *gtest.T) {
+		prefix := fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort())
+		client := g.Client().ContentJson()
+		client.SetPrefix(prefix)
+
+		t.Assert(client.PostContent(ctx, "/test", `{"pagination":false,"name":"","number":0}`), `{"page":1,"pageSize":10,"pagination":false,"name":"","number":0}`)
+		t.Assert(client.PostContent(ctx, "/test"), `{"page":1,"pageSize":10,"pagination":true,"name":"john","number":1}`)
+	})
+}
diff --git a/util/gvalid/gvalid_z_unit_issue_test.go b/util/gvalid/gvalid_z_unit_issue_test.go
index 3e9490226..80449e342 100644
--- a/util/gvalid/gvalid_z_unit_issue_test.go
+++ b/util/gvalid/gvalid_z_unit_issue_test.go
@@ -9,6 +9,7 @@ package gvalid_test
 import (
 	"context"
 	"fmt"
+	"runtime"
 	"testing"
 	"time"
 
@@ -114,3 +115,37 @@ func Test_Issue3636(t *testing.T) {
 		)
 	})
 }
+
+// https://github.com/gogf/gf/issues/4092
+func Test_Issue4092(t *testing.T) {
+	type Model struct {
+		Raw  []byte `v:"required"`
+		Test []byte `v:"foreach|in:1,2,3"`
+	}
+	gtest.C(t, func(t *gtest.T) {
+		const kb = 1024
+		const mb = 1024 * kb
+		raw := make([]byte, 50*mb)
+		in := &Model{
+			Raw:  raw,
+			Test: []byte{40, 5, 6},
+		}
+		err := g.Validator().
+			Data(in).
+			Run(context.Background())
+		t.Assert(err, "The Test value `6` is not in acceptable range: 1,2,3")
+		allocMb := getMemAlloc()
+		t.AssertLE(allocMb, 110)
+	})
+}
+
+func getMemAlloc() uint64 {
+	byteToMb := func(b uint64) uint64 {
+		return b / 1024 / 1024
+	}
+	var m runtime.MemStats
+	runtime.ReadMemStats(&m)
+	// For info on each, see: https://golang.org/pkg/runtime/#MemStats
+	alloc := byteToMb(m.Alloc)
+	return alloc
+}