Repository: cloudwego/hertz
Pull request #1056 (2024-01-30 13:39:11)

feat: add more default type for binding
#### What type of PR is this?
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
feat
#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
- [ ] The description of this PR title is user-oriented and clear enough for others to understand.
- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.
为参数绑定提供更多的默认类型支持

#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en: 为参数绑定提供更多的默认类型支持
zh(optional): add more default type for binding

- 增加了 map、struct、slice 等类型的默认值处理
- 降低 default value 的优先级，避免在做完 json.unmarshal() 后，default value 将其值覆盖

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

welkeyever: Fix the conflict
archever: "降低 default value 的优先级，避免在做完 json.unmarshal() 后，default value 将其值覆盖"  我也遇到了这个缺陷, 希望能尽快合并
archever: "降低 default value 的优先级，避免在做完 json.unmarshal() 后，default value 将其值覆盖"  我也遇到了这个缺陷, 希望能尽快合并
archever: "降低 default value 的优先级，避免在做完 json.unmarshal() 后，default value 将其值覆盖"  我也遇到了这个缺陷, 希望能尽快合并
FGYFFFF: > "降低 default value 的优先级，避免在做完 json.unmarshal() 后，default value 将其值覆盖" 我也遇到了这个缺陷, 希望能尽快合并

ok, 明天我推进一下合入

Patch:
diff --git a/_typos.toml b/_typos.toml
index 3d3103e..bb673ba 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -18,4 +18,8 @@ HeaderReferer = "HeaderReferer"
 expectedReferer = "expectedReferer"
 Referer = "Referer"
 O_WRONLY = "O_WRONLY"
-WRONLY = "WRONLY"
\ No newline at end of file
+WRONLY = "WRONLY"
+ome = "ome"
+ifModifiedSice = "ifModifiedSice"
+hd = "hd"
+pn = "pn"
\ No newline at end of file
diff --git a/pkg/app/server/binding/internal/decoder/base_type_decoder.go b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
index ece04f7..9c5cb12 100644
--- a/pkg/app/server/binding/internal/decoder/base_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
@@ -69,14 +69,17 @@ func (d *baseTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Pa
 	var defaultValue string
 	for _, tagInfo := range d.tagInfos {
 		if tagInfo.Skip || tagInfo.Key == jsonTag || tagInfo.Key == fileNameTag {
-			defaultValue = tagInfo.Default
 			if tagInfo.Key == jsonTag {
+				defaultValue = tagInfo.Default
 				found := checkRequireJSON(req, tagInfo)
 				if found {
 					err = nil
 				} else {
 					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request body does not have this parameter '%s'", d.fieldName, tagInfo.JSONName)
 				}
+				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
+					defaultValue = ""
+				}
 			}
 			continue
 		}
@@ -94,7 +97,7 @@ func (d *baseTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Pa
 		return err
 	}
 	if len(text) == 0 && len(defaultValue) != 0 {
-		text = defaultValue
+		text = toDefaultValue(d.fieldType, defaultValue)
 	}
 	if !exist && len(text) == 0 {
 		return nil
diff --git a/pkg/app/server/binding/internal/decoder/customized_type_decoder.go b/pkg/app/server/binding/internal/decoder/customized_type_decoder.go
index 19efa46..ab343c8 100644
--- a/pkg/app/server/binding/internal/decoder/customized_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/customized_type_decoder.go
@@ -60,7 +60,12 @@ func (d *customizedFieldTextDecoder) Decode(req *protocol.Request, params param.
 	var defaultValue string
 	for _, tagInfo := range d.tagInfos {
 		if tagInfo.Skip || tagInfo.Key == jsonTag || tagInfo.Key == fileNameTag {
-			defaultValue = tagInfo.Default
+			if tagInfo.Key == jsonTag {
+				defaultValue = tagInfo.Default
+				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
+					defaultValue = ""
+				}
+			}
 			continue
 		}
 		text, exist = tagInfo.Getter(req, params, tagInfo.Value)
@@ -73,7 +78,7 @@ func (d *customizedFieldTextDecoder) Decode(req *protocol.Request, params param.
 		return nil
 	}
 	if len(text) == 0 && len(defaultValue) != 0 {
-		text = defaultValue
+		text = toDefaultValue(d.fieldType, defaultValue)
 	}
 
 	v, err := d.decodeFunc(req, params, text)
diff --git a/pkg/app/server/binding/internal/decoder/decoder.go b/pkg/app/server/binding/internal/decoder/decoder.go
index f18a681..4c36bce 100644
--- a/pkg/app/server/binding/internal/decoder/decoder.go
+++ b/pkg/app/server/binding/internal/decoder/decoder.go
@@ -124,7 +124,7 @@ func getFieldDecoder(pInfo parentInfos, field reflect.StructField, index int, by
 	// JSONName is like 'a.b.c' for 'required validate'
 	fieldTagInfos, newParentJSONName, needValidate := lookupFieldTags(field, pInfo.JSONName, config)
 	if len(fieldTagInfos) == 0 && !config.DisableDefaultTag {
-		fieldTagInfos = getDefaultFieldTags(field)
+		fieldTagInfos, newParentJSONName = getDefaultFieldTags(field, pInfo.JSONName)
 	}
 	if len(byTag) != 0 {
 		fieldTagInfos = getFieldTagInfoByTag(field, byTag)
diff --git a/pkg/app/server/binding/internal/decoder/gjson_required.go b/pkg/app/server/binding/internal/decoder/gjson_required.go
index 95e3c4f..130d65d 100644
--- a/pkg/app/server/binding/internal/decoder/gjson_required.go
+++ b/pkg/app/server/binding/internal/decoder/gjson_required.go
@@ -47,3 +47,12 @@ func checkRequireJSON(req *protocol.Request, tagInfo TagInfo) bool {
 	}
 	return true
 }
+
+func keyExist(req *protocol.Request, tagInfo TagInfo) bool {
+	ct := bytesconv.B2s(req.Header.ContentType())
+	if utils.FilterContentType(ct) != consts.MIMEApplicationJSON {
+		return false
+	}
+	result := gjson.GetBytes(req.Body(), tagInfo.JSONName)
+	return result.Exists()
+}
diff --git a/pkg/app/server/binding/internal/decoder/map_type_decoder.go b/pkg/app/server/binding/internal/decoder/map_type_decoder.go
index 31fe85a..59fed77 100644
--- a/pkg/app/server/binding/internal/decoder/map_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/map_type_decoder.go
@@ -61,14 +61,17 @@ func (d *mapTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Par
 	var defaultValue string
 	for _, tagInfo := range d.tagInfos {
 		if tagInfo.Skip || tagInfo.Key == jsonTag || tagInfo.Key == fileNameTag {
-			defaultValue = tagInfo.Default
 			if tagInfo.Key == jsonTag {
+				defaultValue = tagInfo.Default
 				found := checkRequireJSON(req, tagInfo)
 				if found {
 					err = nil
 				} else {
 					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
 				}
+				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
+					defaultValue = ""
+				}
 			}
 			continue
 		}
@@ -86,7 +89,7 @@ func (d *mapTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Par
 		return err
 	}
 	if len(text) == 0 && len(defaultValue) != 0 {
-		text = defaultValue
+		text = toDefaultValue(d.fieldType, defaultValue)
 	}
 	if !exist && len(text) == 0 {
 		return nil
diff --git a/pkg/app/server/binding/internal/decoder/slice_type_decoder.go b/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
index fc5c981..c2887d1 100644
--- a/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/slice_type_decoder.go
@@ -61,16 +61,20 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 	var texts []string
 	var defaultValue string
 	var bindRawBody bool
+	var isDefault bool
 	for _, tagInfo := range d.tagInfos {
 		if tagInfo.Skip || tagInfo.Key == jsonTag || tagInfo.Key == fileNameTag {
-			defaultValue = tagInfo.Default
 			if tagInfo.Key == jsonTag {
+				defaultValue = tagInfo.Default
 				found := checkRequireJSON(req, tagInfo)
 				if found {
 					err = nil
 				} else {
 					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
 				}
+				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) { //
+					defaultValue = ""
+				}
 			}
 			continue
 		}
@@ -91,7 +95,9 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 		return err
 	}
 	if len(texts) == 0 && len(defaultValue) != 0 {
+		defaultValue = toDefaultValue(d.fieldType, defaultValue)
 		texts = append(texts, defaultValue)
+		isDefault = true
 	}
 	if len(texts) == 0 {
 		return nil
@@ -113,7 +119,7 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 	}
 
 	if d.isArray {
-		if len(texts) != field.Len() {
+		if len(texts) != field.Len() && !isDefault {
 			return fmt.Errorf("%q is not valid value for %s", texts, field.Type().String())
 		}
 	} else {
@@ -135,6 +141,13 @@ func (d *sliceTypeFieldTextDecoder) Decode(req *protocol.Request, params param.P
 		elemKind = t.Kind()
 		ptrDepth++
 	}
+	if isDefault {
+		err = hJson.Unmarshal(bytesconv.S2b(texts[0]), reqValue.Field(d.index).Addr().Interface())
+		if err != nil {
+			return fmt.Errorf("using '%s' to unmarshal field '%s: %s' failed, %v", texts[0], d.fieldName, d.fieldType.String(), err)
+		}
+		return nil
+	}
 
 	for idx, text := range texts {
 		var vv reflect.Value
@@ -218,33 +231,3 @@ func getSliceFieldDecoder(field reflect.StructField, index int, tagInfos []TagIn
 		isArray: isArray,
 	}}, nil
 }
-
-func stringToValue(elemType reflect.Type, text string, req *protocol.Request, params param.Params, config *DecodeConfig) (v reflect.Value, err error) {
-	v = reflect.New(elemType).Elem()
-	if customizedFunc, exist := config.TypeUnmarshalFuncs[elemType]; exist {
-		val, err := customizedFunc(req, params, text)
-		if err != nil {
-			return reflect.Value{}, err
-		}
-		return val, nil
-	}
-	switch elemType.Kind() {
-	case reflect.Struct:
-		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
-	case reflect.Map:
-		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
-	case reflect.Array, reflect.Slice:
-		// do nothing
-	default:
-		decoder, err := SelectTextDecoder(elemType)
-		if err != nil {
-			return reflect.Value{}, fmt.Errorf("unsupported type %s for slice/array", elemType.String())
-		}
-		err = decoder.UnmarshalString(text, v, config.LooseZeroMode)
-		if err != nil {
-			return reflect.Value{}, fmt.Errorf("unable to decode '%s' as %s: %w", text, elemType.String(), err)
-		}
-	}
-
-	return v, err
-}
diff --git a/pkg/app/server/binding/internal/decoder/sonic_required.go b/pkg/app/server/binding/internal/decoder/sonic_required.go
index e408901..61f3d8d 100644
--- a/pkg/app/server/binding/internal/decoder/sonic_required.go
+++ b/pkg/app/server/binding/internal/decoder/sonic_required.go
@@ -60,3 +60,12 @@ func stringSliceForInterface(s string) (ret []interface{}) {
 	}
 	return
 }
+
+func keyExist(req *protocol.Request, tagInfo TagInfo) bool {
+	ct := bytesconv.B2s(req.Header.ContentType())
+	if utils.FilterContentType(ct) != consts.MIMEApplicationJSON {
+		return false
+	}
+	node, _ := sonic.Get(req.Body(), stringSliceForInterface(tagInfo.JSONName)...)
+	return node.Exists()
+}
diff --git a/pkg/app/server/binding/internal/decoder/struct_type_decoder.go b/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
index 3030f2a..75f3ae4 100644
--- a/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/struct_type_decoder.go
@@ -38,14 +38,17 @@ func (d *structTypeFieldTextDecoder) Decode(req *protocol.Request, params param.
 	var defaultValue string
 	for _, tagInfo := range d.tagInfos {
 		if tagInfo.Skip || tagInfo.Key == jsonTag || tagInfo.Key == fileNameTag {
-			defaultValue = tagInfo.Default
 			if tagInfo.Key == jsonTag {
+				defaultValue = tagInfo.Default
 				found := checkRequireJSON(req, tagInfo)
 				if found {
 					err = nil
 				} else {
 					err = fmt.Errorf("'%s' field is a 'required' parameter, but the request does not have this parameter", d.fieldName)
 				}
+				if len(tagInfo.Default) != 0 && keyExist(req, tagInfo) {
+					defaultValue = ""
+				}
 			}
 			continue
 		}
@@ -63,7 +66,7 @@ func (d *structTypeFieldTextDecoder) Decode(req *protocol.Request, params param.
 		return err
 	}
 	if len(text) == 0 && len(defaultValue) != 0 {
-		text = defaultValue
+		text = toDefaultValue(d.fieldType, defaultValue)
 	}
 	if !exist && len(text) == 0 {
 		return nil
diff --git a/pkg/app/server/binding/internal/decoder/tag.go b/pkg/app/server/binding/internal/decoder/tag.go
index 6df09aa..8ca5ae0 100644
--- a/pkg/app/server/binding/internal/decoder/tag.go
+++ b/pkg/app/server/binding/internal/decoder/tag.go
@@ -87,7 +87,7 @@ func lookupFieldTags(field reflect.StructField, parentJSONName string, config *D
 			tagValue = field.Name
 		}
 		skip := false
-		jsonName := ""
+		jsonName := parentJSONName + "." + field.Name
 		if tag == jsonTag {
 			jsonName = parentJSONName + "." + tagValue
 		}
@@ -120,7 +120,7 @@ func lookupFieldTags(field reflect.StructField, parentJSONName string, config *D
 	return tagInfos, newParentJSONName, needValidate
 }
 
-func getDefaultFieldTags(field reflect.StructField) (tagInfos []TagInfo) {
+func getDefaultFieldTags(field reflect.StructField, parentJSONName string) (tagInfos []TagInfo, newParentJSONName string) {
 	defaultVal := ""
 	if val, ok := field.Tag.Lookup(defaultTag); ok {
 		defaultVal = val
@@ -128,8 +128,10 @@ func getDefaultFieldTags(field reflect.StructField) (tagInfos []TagInfo) {
 
 	tags := []string{pathTag, formTag, queryTag, cookieTag, headerTag, jsonTag, fileNameTag}
 	for _, tag := range tags {
-		tagInfos = append(tagInfos, TagInfo{Key: tag, Value: field.Name, Default: defaultVal})
+		jsonName := strings.TrimPrefix(parentJSONName+"."+field.Name, ".")
+		tagInfos = append(tagInfos, TagInfo{Key: tag, Value: field.Name, Default: defaultVal, JSONName: jsonName})
 	}
+	newParentJSONName = strings.TrimPrefix(parentJSONName+"."+field.Name, ".")
 
 	return
 }
diff --git a/pkg/app/server/binding/internal/decoder/util.go b/pkg/app/server/binding/internal/decoder/util.go
new file mode 100644
index 0000000..be141c2
--- /dev/null
+++ b/pkg/app/server/binding/internal/decoder/util.go
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package decoder
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/cloudwego/hertz/internal/bytesconv"
+	hJson "github.com/cloudwego/hertz/pkg/common/json"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/route/param"
+)
+
+const (
+	specialChar = "\x07"
+)
+
+// toDefaultValue will preprocess the default value and transfer it to be standard format
+func toDefaultValue(typ reflect.Type, defaultValue string) string {
+	switch typ.Kind() {
+	case reflect.Slice, reflect.Array, reflect.Map, reflect.Struct:
+		// escape single quote and double quote, replace single quote with double quote
+		defaultValue = strings.Replace(defaultValue, `"`, `\"`, -1)
+		defaultValue = strings.Replace(defaultValue, `\'`, specialChar, -1)
+		defaultValue = strings.Replace(defaultValue, `'`, `"`, -1)
+		defaultValue = strings.Replace(defaultValue, specialChar, `'`, -1)
+	}
+	return defaultValue
+}
+
+// stringToValue is used to dynamically create reflect.Value for 'text'
+func stringToValue(elemType reflect.Type, text string, req *protocol.Request, params param.Params, config *DecodeConfig) (v reflect.Value, err error) {
+	v = reflect.New(elemType).Elem()
+	if customizedFunc, exist := config.TypeUnmarshalFuncs[elemType]; exist {
+		val, err := customizedFunc(req, params, text)
+		if err != nil {
+			return reflect.Value{}, err
+		}
+		return val, nil
+	}
+	switch elemType.Kind() {
+	case reflect.Struct:
+		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
+	case reflect.Map:
+		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
+	case reflect.Array, reflect.Slice:
+		// do nothing
+	default:
+		decoder, err := SelectTextDecoder(elemType)
+		if err != nil {
+			return reflect.Value{}, fmt.Errorf("unsupported type %s for slice/array", elemType.String())
+		}
+		err = decoder.UnmarshalString(text, v, config.LooseZeroMode)
+		if err != nil {
+			return reflect.Value{}, fmt.Errorf("unable to decode '%s' as %s: %w", text, elemType.String(), err)
+		}
+	}
+
+	return v, err
+}


Test patch:
diff --git a/pkg/app/server/binding/binder_test.go b/pkg/app/server/binding/binder_test.go
index 7919d8d..225b8bd 100644
--- a/pkg/app/server/binding/binder_test.go
+++ b/pkg/app/server/binding/binder_test.go
@@ -417,10 +417,11 @@ func TestBind_ZeroValueBind(t *testing.T) {
 
 func TestBind_DefaultValueBind(t *testing.T) {
 	var s struct {
-		A int      `default:"15"`
-		B float64  `query:"b" default:"17"`
-		C []int    `default:"15"`
-		D []string `default:"qwe"`
+		A int       `default:"15"`
+		B float64   `query:"b" default:"17"`
+		C []int     `default:"[15]"`
+		D []string  `default:"['qwe','asd']"`
+		F [2]string `default:"['qwe','asd','zxc']"`
 	}
 	req := newMockRequest().
 		SetRequestURI("http://foobar.com")
@@ -432,7 +433,23 @@ func TestBind_DefaultValueBind(t *testing.T) {
 	assert.DeepEqual(t, 15, s.A)
 	assert.DeepEqual(t, float64(17), s.B)
 	assert.DeepEqual(t, 15, s.C[0])
+	assert.DeepEqual(t, 2, len(s.D))
 	assert.DeepEqual(t, "qwe", s.D[0])
+	assert.DeepEqual(t, "asd", s.D[1])
+	assert.DeepEqual(t, 2, len(s.F))
+	assert.DeepEqual(t, "qwe", s.F[0])
+	assert.DeepEqual(t, "asd", s.F[1])
+
+	var s2 struct {
+		F [2]string `default:"['qwe']"`
+	}
+	err = DefaultBinder().Bind(req.Req, &s2, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	assert.DeepEqual(t, 2, len(s2.F))
+	assert.DeepEqual(t, "qwe", s2.F[0])
+	assert.DeepEqual(t, "", s2.F[1])
 
 	var d struct {
 		D [2]string `default:"qwe"`
@@ -1549,6 +1566,32 @@ func TestBind_Issue1015(t *testing.T) {
 	assert.DeepEqual(t, "asd", result.A)
 }
 
+func TestBind_JSONWithDefault(t *testing.T) {
+	type Req struct {
+		J1 string `json:"j1" default:"j1default"`
+	}
+
+	req := newMockRequest().
+		SetJSONContentType().
+		SetBody([]byte(`{"j1":"j1"}`))
+	var result Req
+	err := DefaultBinder().Bind(req.Req, &result, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	assert.DeepEqual(t, "j1", result.J1)
+
+	result = Req{}
+	req = newMockRequest().
+		SetJSONContentType().
+		SetBody([]byte(`{"j2":"j2"}`))
+	err = DefaultBinder().Bind(req.Req, &result, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	assert.DeepEqual(t, "j1default", result.J1)
+}
+
 func TestBind_WithoutPreBindForTag(t *testing.T) {
 	type BaseQuery struct {
 		Action  string `query:"Action" binding:"required"`
diff --git a/pkg/app/server/binding/tagexpr_bind_test.go b/pkg/app/server/binding/tagexpr_bind_test.go
index 8222174..1533b5a 100644
--- a/pkg/app/server/binding/tagexpr_bind_test.go
+++ b/pkg/app/server/binding/tagexpr_bind_test.go
@@ -539,38 +539,37 @@ func TestPath(t *testing.T) {
 	assert.DeepEqual(t, (*int64)(nil), recv.Z)
 }
 
-// FIXME: 复杂类型的默认值，暂时先不做，低优
 func TestDefault(t *testing.T) {
-	//type S struct {
-	//	SS string `json:"ss"`
-	//}
+	type S struct {
+		SS string `json:"ss"`
+	}
 	type Recv struct {
 		X **struct {
-			A []string `path:"a" json:"a"`
-			B int32    `path:"b" default:"32"`
-			C bool     `json:"c" default:"true"`
-			D *float32 `default:"123.4"`
-			// E          *[]string          `default:"['a','b','c','d,e,f']"`
-			// F          map[string]string  `default:"{'a':'\"\\'1','\"b':'c','c':'2'}"`
-			// G          map[string]int64   `default:"{'a':1,'b':2,'c':3}"`
-			// H          map[string]float64 `default:"{'a':0.1,'b':1.2,'c':2.3}"`
-			// I          map[string]float64 `default:"{'\"a\"':0.1,'b':1.2,'c':2.3}"`
-			Empty      string `default:""`
-			Null       string `default:""`
-			CommaSpace string `default:",a:c "`
-			Dash       string `default:"-"`
+			A          []string           `path:"a" json:"a"`
+			B          int32              `path:"b" default:"32"`
+			C          bool               `json:"c" default:"true"`
+			D          *float32           `default:"123.4"`
+			E          *[]string          `default:"['a','b','c','d,e,f']"`
+			F          map[string]string  `default:"{'a':'\"\\'1','\"b':'c','c':'2'}"`
+			G          map[string]int64   `default:"{'a':1,'b':2,'c':3}"`
+			H          map[string]float64 `default:"{'a':0.1,'b':1.2,'c':2.3}"`
+			I          map[string]float64 `default:"{'\"a\"':0.1,'b':1.2,'c':2.3}"`
+			Empty      string             `default:""`
+			Null       string             `default:""`
+			CommaSpace string             `default:",a:c "`
+			Dash       string             `default:"-"`
 			// InvalidInt int                `default:"abc"`
 			// InvalidMap map[string]string  `default:"abc"`
 		}
-		Y string `json:"y" default:"y1"`
-		Z int64
-		W string `json:"w"`
-		// V []int64   `json:"u" default:"[1,2,3]"`
-		// U []float32 `json:"u" default:"[1.1,2,3]"`
-		T *string `json:"t" default:"t1"`
-		// S S         `default:"{'ss':'test'}"`
-		// O *S        `default:"{'ss':'test2'}"`
-		// Complex map[string][]map[string][]int64 `default:"{'a':[{'aa':[1,2,3], 'bb':[4,5]}],'b':[{}]}"`
+		Y       string `json:"y" default:"y1"`
+		Z       int64
+		W       string                          `json:"w"`
+		V       []int64                         `json:"v" default:"[1,2,3]"`
+		U       []float32                       `json:"u" default:"[1.1,2,3]"`
+		T       *string                         `json:"t" default:"t1"`
+		S       S                               `default:"{'ss':'test'}"`
+		O       *S                              `default:"{'ss':'test2'}"`
+		Complex map[string][]map[string][]int64 `default:"{'a':[{'aa':[1,2,3], 'bb':[4,5]}],'b':[{}]}"`
 	}
 
 	bodyReader := strings.NewReader(`{
@@ -601,11 +600,11 @@ func TestDefault(t *testing.T) {
 	assert.DeepEqual(t, int32(32), (**recv.X).B)
 	assert.DeepEqual(t, true, (**recv.X).C)
 	assert.DeepEqual(t, float32(123.4), *(**recv.X).D)
-	// assert.DeepEqual(t, []string{"a", "b", "c", "d,e,f"}, *(**recv.X).E)
-	// assert.DeepEqual(t, map[string]string{"a": "\"'1", "\"b": "c", "c": "2"}, (**recv.X).F)
-	// assert.DeepEqual(t, map[string]int64{"a": 1, "b": 2, "c": 3}, (**recv.X).G)
-	// assert.DeepEqual(t, map[string]float64{"a": 0.1, "b": 1.2, "c": 2.3}, (**recv.X).H)
-	// assert.DeepEqual(t, map[string]float64{"\"a\"": 0.1, "b": 1.2, "c": 2.3}, (**recv.X).I)
+	assert.DeepEqual(t, []string{"a", "b", "c", "d,e,f"}, *(**recv.X).E)
+	assert.DeepEqual(t, map[string]string{"a": "\"'1", "\"b": "c", "c": "2"}, (**recv.X).F)
+	assert.DeepEqual(t, map[string]int64{"a": 1, "b": 2, "c": 3}, (**recv.X).G)
+	assert.DeepEqual(t, map[string]float64{"a": 0.1, "b": 1.2, "c": 2.3}, (**recv.X).H)
+	assert.DeepEqual(t, map[string]float64{"\"a\"": 0.1, "b": 1.2, "c": 2.3}, (**recv.X).I)
 	assert.DeepEqual(t, "", (**recv.X).Empty)
 	assert.DeepEqual(t, "", (**recv.X).Null)
 	assert.DeepEqual(t, ",a:c ", (**recv.X).CommaSpace)
@@ -615,11 +614,11 @@ func TestDefault(t *testing.T) {
 	assert.DeepEqual(t, "y1", recv.Y)
 	assert.DeepEqual(t, "t1", *recv.T)
 	assert.DeepEqual(t, int64(6), recv.Z)
-	// assert.DeepEqual(t, []int64{1, 2, 3}, recv.V)
-	// assert.DeepEqual(t, []float32{1.1, 2, 3}, recv.U)
-	// assert.DeepEqual(t, S{SS: "test"}, recv.S)
-	// assert.DeepEqual(t, &S{SS: "test2"}, recv.O)
-	// assert.DeepEqual(t, map[string][]map[string][]int64{"a": {{"aa": {1, 2, 3}, "bb": []int64{4, 5}}}, "b": {map[string][]int64{}}}, recv.Complex)
+	assert.DeepEqual(t, []int64{1, 2, 3}, recv.V)
+	assert.DeepEqual(t, []float32{1.1, 2, 3}, recv.U)
+	assert.DeepEqual(t, S{SS: "test"}, recv.S)
+	assert.DeepEqual(t, &S{SS: "test2"}, recv.O)
+	assert.DeepEqual(t, map[string][]map[string][]int64{"a": {{"aa": {1, 2, 3}, "bb": []int64{4, 5}}}, "b": {map[string][]int64{}}}, recv.Complex)
 }
 
 func TestAuto(t *testing.T) {
@@ -1196,29 +1195,29 @@ func TestIssue26(t *testing.T) {
 	assert.DeepEqual(t, recv, recv2)
 }
 
-// FIXME: after 'json unmarshal', the default value will change it
-//func TestDefault2(t *testing.T) {
-//	type Recv struct {
-//		X **struct {
-//			Dash string `default:"xxxx"`
-//		}
-//	}
-//	bodyReader := strings.NewReader(`{
-//		"X": {
-//			"Dash": "hello Dash"
-//		}
-//	}`)
-//	header := make(http.Header)
-//	header.Set("Content-Type", consts.MIMEApplicationJSON)
-//	req := newRequest("", header, nil, bodyReader)
-//	recv := new(Recv)
-//
-//	err := DefaultBinder().Bind(req.Req, nil, recv)
-//	if err != nil {
-//		t.Error(err)
-//	}
-//	assert.DeepEqual(t, "hello Dash", (**recv.X).Dash)
-//}
+// BUGFIX: after 'json unmarshal', the default value will change it
+func TestDefault2(t *testing.T) {
+	type Recv struct {
+		X **struct {
+			Dash string `default:"xxxx"`
+		}
+	}
+	bodyReader := strings.NewReader(`{
+		"X": {
+			"Dash": "hello Dash"
+		}
+	}`)
+	header := make(http.Header)
+	header.Set("Content-Type", consts.MIMEApplicationJSON)
+	req := newRequest("", header, nil, bodyReader)
+	recv := new(Recv)
+
+	err := DefaultBinder().Bind(req.Req, recv, nil)
+	if err != nil {
+		t.Error(err)
+	}
+	assert.DeepEqual(t, "hello Dash", (**recv.X).Dash)
+}
 
 type (
 	files map[string][]file