Repository: zeromicro/go-zero (go)

Issue:
About the updated form array in v1.7.5
When I Declaer 

var v struct {
  Codes []string `form:"Codes"`
}
If a code is comma separated like aaa,bbb,ccc. When I enter parameter ```? codes=aaa,bbb,ccc&codes=ccc,ddd,eee```It is normal but when I enter only one ```? codes=aaa,bbb,ccc``` And it blew up.
So I think we should use the tag to turn this comma-separated array parsing on or off
![Image](https://github.com/user-attachments/assets/b4a23683-b2a0-478c-9602-03ab0d174d35)

![Image](https://github.com/user-attachments/assets/e24bde86-1a68-4c94-b77a-6f44c3f04992)

Pull request description:
feat add tag switch to disable form array of split comma format
Resolve #4629 

Patch:
diff --git a/core/mapping/fieldoptions.go b/core/mapping/fieldoptions.go
index 14b3c84b..7ba0eae4 100644
--- a/core/mapping/fieldoptions.go
+++ b/core/mapping/fieldoptions.go
@@ -8,13 +8,14 @@ type (
 	// use context and OptionalDep option to determine the value of Optional
 	// nothing to do with context.Context
 	fieldOptionsWithContext struct {
-		Inherit    bool
-		FromString bool
-		Optional   bool
-		Options    []string
-		Default    string
-		EnvVar     string
-		Range      *numberRange
+		Inherit        bool
+		FromString     bool
+		Optional       bool
+		Options        []string
+		Default        string
+		EnvVar         string
+		Range          *numberRange
+		FormArrayComma bool
 	}
 
 	fieldOptions struct {
diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go
index 8d783986..32dce6ef 100644
--- a/core/mapping/unmarshaler.go
+++ b/core/mapping/unmarshaler.go
@@ -131,7 +131,7 @@ func (u *Unmarshaler) fillMapFromString(value reflect.Value, mapValue any) error
 }
 
 func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
-	mapValue any, fullName string) error {
+	mapValue any, fullName string, opts *fieldOptionsWithContext) error {
 	if !value.CanSet() {
 		return errValueNotSettable
 	}
@@ -153,7 +153,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
 	}
 
 	if u.opts.fromArray {
-		refValue = makeStringSlice(refValue)
+		refValue = makeStringSlice(refValue, opts)
 	}
 
 	var valid bool
@@ -174,7 +174,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
 				return err
 			}
 		case reflect.Slice:
-			if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName); err != nil {
+			if err := u.fillSlice(dereffedBaseType, conv.Index(i), ithValue, sliceFullName, opts); err != nil {
 				return err
 			}
 		default:
@@ -288,7 +288,7 @@ func (u *Unmarshaler) fillSliceWithDefault(derefedType reflect.Type, value refle
 		defaultCacheLock.Unlock()
 	}
 
-	return u.fillSlice(derefedType, value, slice, fullName)
+	return u.fillSlice(derefedType, value, slice, fullName, nil)
 }
 
 func (u *Unmarshaler) fillStructElement(baseType reflect.Type, target reflect.Value,
@@ -359,7 +359,7 @@ func (u *Unmarshaler) generateMap(keyType, elemType reflect.Type, mapValue any,
 		switch dereffedElemKind {
 		case reflect.Slice:
 			target := reflect.New(dereffedElemType)
-			if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName); err != nil {
+			if err := u.fillSlice(elemType, target.Elem(), keythData, mapFullName, nil); err != nil {
 				return emptyValue, err
 			}
 
@@ -604,7 +604,7 @@ func (u *Unmarshaler) processFieldNotFromString(fieldType reflect.Type, value re
 			parent:  vp.parent,
 		}, fullName)
 	case typeKind == reflect.Slice && valueKind == reflect.Slice:
-		return u.fillSlice(fieldType, value, mapValue, fullName)
+		return u.fillSlice(fieldType, value, mapValue, fullName, opts)
 	case valueKind == reflect.Map && typeKind == reflect.Map:
 		return u.fillMap(fieldType, value, mapValue, fullName)
 	case valueKind == reflect.String && typeKind == reflect.Map:
@@ -985,7 +985,7 @@ func (u *Unmarshaler) unmarshal(i, v any, fullName string) error {
 			return errTypeMismatch
 		}
 
-		return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName)
+		return u.fillSlice(elemType, reflect.ValueOf(v).Elem(), iv, fullName, nil)
 	default:
 		return errUnsupportedType
 	}
@@ -1189,7 +1189,7 @@ func join(elem ...string) string {
 	return builder.String()
 }
 
-func makeStringSlice(refValue reflect.Value) reflect.Value {
+func makeStringSlice(refValue reflect.Value, opts *fieldOptionsWithContext) reflect.Value {
 	if refValue.Len() != 1 {
 		return refValue
 	}
@@ -1203,19 +1203,23 @@ func makeStringSlice(refValue reflect.Value) reflect.Value {
 	if !ok {
 		return refValue
 	}
+	// comma mode is on by default or display designations are split by commas
+	if opts == nil || opts.FormArrayComma {
+		splits := strings.Split(val, comma)
+		if len(splits) <= 1 {
+			return refValue
+		}
 
-	splits := strings.Split(val, comma)
-	if len(splits) <= 1 {
+		slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
+		for i, split := range splits {
+			// allow empty strings
+			slice.Index(i).Set(reflect.ValueOf(split))
+		}
+		return slice
+	} else {
 		return refValue
 	}
 
-	slice := reflect.MakeSlice(stringSliceType, len(splits), len(splits))
-	for i, split := range splits {
-		// allow empty strings
-		slice.Index(i).Set(reflect.ValueOf(split))
-	}
-
-	return slice
 }
 
 func newInitError(name string) error {
diff --git a/core/mapping/utils.go b/core/mapping/utils.go
index f84167e3..f6a1666d 100644
--- a/core/mapping/utils.go
+++ b/core/mapping/utils.go
@@ -22,6 +22,7 @@ const (
 	optionalOption     = "optional"
 	optionsOption      = "options"
 	rangeOption        = "range"
+	formArrayComma     = "arrayComma"
 	optionSeparator    = "|"
 	equalToken         = "="
 	escapeChar         = '\\'
@@ -160,6 +161,10 @@ func doParseKeyAndOptions(field reflect.StructField, value string) (string, *fie
 	}
 
 	var fieldOpts fieldOptions
+
+	// The comma split form array mode was enabled in 1.7.5
+	// so the default value is true in order not to introduce destructiveness
+	fieldOpts.FormArrayComma = true
 	for _, segment := range options {
 		option := strings.TrimSpace(segment)
 		if err := parseOption(&fieldOpts, field.Name, option); err != nil {
@@ -410,6 +415,12 @@ func parseOption(fieldOpts *fieldOptions, fieldName, option string) error {
 		}
 
 		fieldOpts.Range = nr
+	case strings.HasPrefix(option, formArrayComma):
+		val, err := parseEqBoolOption(fieldName, formArrayComma, option)
+		if err != nil {
+			return err
+		}
+		fieldOpts.FormArrayComma = val
 	}
 
 	return nil
@@ -437,7 +448,21 @@ func parseProperty(field, tag, val string) (string, error) {
 
 	return strings.TrimSpace(segs[1]), nil
 }
-
+func parseEqBoolOption(field, tag, val string) (bool, error) {
+	segs := strings.Split(val, equalToken)
+	switch len(segs) {
+	case 1:
+		return true, nil
+	case 2:
+		parseBool, err := strconv.ParseBool(segs[1])
+		if err != nil {
+			return false, fmt.Errorf("field %q has wrong %s", field, tag)
+		}
+		return parseBool, nil
+	default:
+		return false, fmt.Errorf("field %q has wrong %s", field, tag)
+	}
+}
 func parseSegments(val string) []string {
 	var segments []string
 	var escaped, grouped bool


Test patch:
diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go
index c81a5707..0cf4df2f 100644
--- a/rest/httpx/requests_test.go
+++ b/rest/httpx/requests_test.go
@@ -268,6 +268,61 @@ func TestParseFormArray(t *testing.T) {
 			assert.ElementsMatch(t, []float64{2}, v.Numbers)
 		}
 	})
+	t.Run("slice with one value on  disable array of comma split format", func(t *testing.T) {
+		var v struct {
+			Codes []string `form:"codes,arrayComma=false"`
+		}
+		r, err := http.NewRequest(
+			http.MethodGet,
+			"/a?codes=aaa,bbb,ccc",
+			http.NoBody)
+		assert.NoError(t, err)
+		if assert.NoError(t, Parse(r, &v)) {
+			assert.ElementsMatch(t, []string{"aaa,bbb,ccc"}, v.Codes)
+		}
+	})
+	t.Run("slice with multiple value on  disable array of comma split format", func(t *testing.T) {
+		var v struct {
+			Codes []string `form:"codes,arrayComma=false"`
+		}
+
+		r, err := http.NewRequest(
+			http.MethodGet,
+			"/a?codes=aaa,bbb,ccc&codes=ccc,ddd,eee",
+			http.NoBody)
+		assert.NoError(t, err)
+		if assert.NoError(t, Parse(r, &v)) {
+			assert.ElementsMatch(t, []string{"aaa,bbb,ccc", "ccc,ddd,eee"}, v.Codes)
+		}
+	})
+	t.Run("slice with multiple value on enable array of comma split format", func(t *testing.T) {
+		var v struct {
+			Codes []string `form:"codes,arrayComma=true"`
+		}
+
+		r, err := http.NewRequest(
+			http.MethodGet,
+			"/a?codes=aaa,bbb,ccc&codes=ccc,ddd,eee",
+			http.NoBody)
+		assert.NoError(t, err)
+		if assert.NoError(t, Parse(r, &v)) {
+			assert.ElementsMatch(t, []string{"aaa,bbb,ccc", "ccc,ddd,eee"}, v.Codes)
+		}
+	})
+	t.Run("slice with one value on enable array of comma split format", func(t *testing.T) {
+		var v struct {
+			Codes []string `form:"codes,arrayComma=true"`
+		}
+
+		r, err := http.NewRequest(
+			http.MethodGet,
+			"/a?codes=aaa,bbb,ccc",
+			http.NoBody)
+		assert.NoError(t, err)
+		if assert.NoError(t, Parse(r, &v)) {
+			assert.ElementsMatch(t, []string{"aaa", "bbb", "ccc"}, v.Codes)
+		}
+	})
 }
 
 func TestParseForm_Error(t *testing.T) {