Repository: karmada-io/karmada

Issue:
Fix Karmada Operator API `URL` consistency issue
**Task description**:
The [URL field](https://github.com/karmada-io/karmada/blob/4594ca0b4b6fcdf0eb3678a0d93896ddc96aaa9a/operator/pkg/apis/operator/v1alpha1/type.go#L62) is marked as required with `+required`, but its JSON tag includes `omitempty`.  For required fields, it's better to omit omitempty to ensure the field is always present in the serialized JSON. This improves API consistency and predictability.

**Solution**:
1. Make changes to the API as follows:
```diff
--- a/operator/pkg/apis/operator/v1alpha1/type.go
+++ b/operator/pkg/apis/operator/v1alpha1/type.go
@@ -59,7 +59,7 @@ const (
 type HTTPSource struct {
        // URL specifies the URL of the CRD tarball resource.
        // +required
-       URL string `json:"url,omitempty"`
+       URL string `json:"url"`

        // Proxy specifies the configuration of a proxy server to use when downloading the CRD tarball.
        // When set, the operator will use the configuration to determine how to establish a connection to the proxy to fetch the tarball from the URL specified above.
``` 
2. Then update the auto-generated files by command `make update` .
3. Commit all files and send the PR.

**Who can join or take the task**:

The good first issue is intended for `first-time contributors` to get started on his/her contributor journey.

After a contributor has successfully completed 1-2 good first issue's,
they should be ready to move on to `help wanted` items, saving the remaining `good first issue` for other new contributors.

**How to join or take the task**:

Just reply on the issue with the message `/assign` in a separate line.

Then, the issue will be assigned to you.

**How to ask for help**:

If you need help or have questions, please feel free to ask on this issue.
The issue author or other members of the community will guide you through the contribution process.

Hints from issue comments:
@rayo1uo: /assign

Solution patch:
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index 5598f0cc9..c786058ba 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -18894,7 +18894,8 @@
       "properties": {
         "name": {
           "description": "Name is the name of the cluster to be selected.",
-          "type": "string"
+          "type": "string",
+          "default": ""
         }
       }
     },
diff --git a/operator/pkg/apis/operator/v1alpha1/type.go b/operator/pkg/apis/operator/v1alpha1/type.go
index 52b4a1b44..f184a4281 100644
--- a/operator/pkg/apis/operator/v1alpha1/type.go
+++ b/operator/pkg/apis/operator/v1alpha1/type.go
@@ -59,7 +59,7 @@ const (
 type HTTPSource struct {
 	// URL specifies the URL of the CRD tarball resource.
 	// +required
-	URL string `json:"url,omitempty"`
+	URL string `json:"url"`
 
 	// Proxy specifies the configuration of a proxy server to use when downloading the CRD tarball.
 	// When set, the operator will use the configuration to determine how to establish a connection to the proxy to fetch the tarball from the URL specified above.
@@ -179,7 +179,7 @@ type ImageRegistry struct {
 type KarmadaComponents struct {
 	// Etcd holds configuration for etcd.
 	// +required
-	Etcd *Etcd `json:"etcd,omitempty"`
+	Etcd *Etcd `json:"etcd"`
 
 	// KarmadaAPIServer holds settings to kube-apiserver component. Currently, kube-apiserver
 	// is used as the apiserver of karmada. we had the same experience as k8s apiserver.
diff --git a/pkg/apis/networking/v1alpha1/service_types.go b/pkg/apis/networking/v1alpha1/service_types.go
index 15fc084fc..1dbdd6a98 100644
--- a/pkg/apis/networking/v1alpha1/service_types.go
+++ b/pkg/apis/networking/v1alpha1/service_types.go
@@ -112,7 +112,7 @@ type MultiClusterServiceSpec struct {
 type ClusterSelector struct {
 	// Name is the name of the cluster to be selected.
 	// +required
-	Name string `json:"name,omitempty"`
+	Name string `json:"name"`
 }
 
 // ExposureType describes how to expose the service.
diff --git a/pkg/apis/policy/v1alpha1/override_types.go b/pkg/apis/policy/v1alpha1/override_types.go
index 6c8b3d47b..84f909b5f 100644
--- a/pkg/apis/policy/v1alpha1/override_types.go
+++ b/pkg/apis/policy/v1alpha1/override_types.go
@@ -148,7 +148,7 @@ type LabelAnnotationOverrider struct {
 	// Items in Value which match in annotations/labels will be deleted when Operator is 'remove'.
 	// Items in Value which match in annotations/labels will be replaced when Operator is 'replace'.
 	// +required
-	Value map[string]string `json:"value,omitempty"`
+	Value map[string]string `json:"value"`
 }
 
 // ImageOverrider represents the rules dedicated to handling image overrides.
diff --git a/pkg/apis/search/searchregistry_types.go b/pkg/apis/search/searchregistry_types.go
index 4389cf18c..0137ca1f2 100644
--- a/pkg/apis/search/searchregistry_types.go
+++ b/pkg/apis/search/searchregistry_types.go
@@ -92,7 +92,7 @@ type OpenSearchConfig struct {
 	// - secret.data.userName
 	// - secret.data.password
 	// +required
-	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef,omitempty"`
+	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef"`
 
 	// More configurations such as transport, index should be added from here.
 }
diff --git a/pkg/apis/search/v1alpha1/searchregistry_types.go b/pkg/apis/search/v1alpha1/searchregistry_types.go
index edbc25fcf..88159dcc2 100644
--- a/pkg/apis/search/v1alpha1/searchregistry_types.go
+++ b/pkg/apis/search/v1alpha1/searchregistry_types.go
@@ -104,7 +104,7 @@ type OpenSearchConfig struct {
 	// - secret.data.userName
 	// - secret.data.password
 	// +required
-	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef,omitempty"`
+	SecretRef clusterv1alpha1.LocalSecretReference `json:"secretRef"`
 
 	// More configurations such as transport, index should be added from here.
 }
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 5be909c56..1676c92dd 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -3083,6 +3083,7 @@ func schema_pkg_apis_networking_v1alpha1_ClusterSelector(ref common.ReferenceCal
 					"name": {
 						SchemaProps: spec.SchemaProps{
 							Description: "Name is the name of the cluster to be selected.",
+							Default:     "",
 							Type:        []string{"string"},
 							Format:      "",
 						},
diff --git a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
index 36b085dca..8115f0744 100644
--- a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
+++ b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert.go
@@ -161,7 +161,7 @@ func traverseToFindEmptyField(root gjson.Result, fieldPath []string) (sets.Set[s
 		// the first element is key=0, value={"ruleName":[]}
 		// however, we expected to record the `ruleName` field path as `rules.ruleName`, rather than `rules.0.ruleName`.
 		if rootIsNotArray {
-			curFieldPath = append(fieldPath, key.String())
+			curFieldPath = append(fieldPath, escapeJSONPathDotAndColon(key.String()))
 		}
 		curFieldStr := strings.Join(curFieldPath, ".")
 
@@ -181,6 +181,16 @@ func traverseToFindEmptyField(root gjson.Result, fieldPath []string) (sets.Set[s
 	return fieldOfEmptySlice, fieldOfEmptyStruct
 }
 
+// escapeJSONPathDotAndColon escape the dot and colon characters in json path key.
+// sjson package use '.' and '.:' to split the json path, so if a json key contains these characters,
+// we need to escape them by '\' first.
+// More details can refer to https://pkg.go.dev/github.com/tidwall/sjson#readme-path-syntax.
+func escapeJSONPathDotAndColon(field string) string {
+	field = strings.ReplaceAll(field, ".", `\.`)
+	field = strings.ReplaceAll(field, ":", `\:`)
+	return field
+}
+
 // traverseToFindEmptyFieldNeededModify find the field with empty values which needed to be modified by traverse a gjson.Result
 //
 // e.g: root={"spec":{"aa":[],"bb":[],"cc":["x"],"dd":{"ee":[],"ff":[]}}}
@@ -208,9 +218,9 @@ func traverseToFindEmptyFieldNeededModify(root gjson.Result, fieldPath, fieldPat
 		// the first element is key=0, value={"ruleName":[]}
 		// we record `rules.ruleName` into curFieldPath, and record `rules.0.ruleName` into curFieldPathWithArrayIndex.
 		if rootIsNotArray {
-			curFieldPath = append(fieldPath, key.String())
+			curFieldPath = append(fieldPath, escapeJSONPathDotAndColon(key.String()))
 		}
-		curFieldPathWithArrayIndex := append(fieldPathWithArrayIndex, key.String())
+		curFieldPathWithArrayIndex := append(fieldPathWithArrayIndex, escapeJSONPathDotAndColon(key.String()))
 
 		if value.IsArray() && len(value.Array()) == 0 {
 			curFieldPathStr := strings.Join(curFieldPath, ".")



Test patch:
diff --git a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
index f6461bbc6..597566b6e 100644
--- a/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
+++ b/pkg/resourceinterpreter/customized/declarative/luavm/lua_convert_test.go
@@ -325,6 +325,25 @@ func Test_traverseToFindEmptyField(t *testing.T) {
 				fieldOfEmptyStruct: sets.New[string]("spec.aa", "spec.dd.ee"),
 			},
 		},
+		{
+			name: "traverse to find empty field with special characters in keys",
+			args: args{
+				root: gjson.Parse(`{
+					"spec": {
+						"replicas": 3,
+						":colons": {},
+						".dots": {},
+						":.colonAndDot": {},
+						".:dotAndColon": {}
+					}
+				}`),
+				fieldPath: nil,
+			},
+			wants: wants{
+				fieldOfEmptySlice:  sets.New[string](),
+				fieldOfEmptyStruct: sets.New[string](`spec.\:colons`, `spec.\.dots`, `spec.\:\.colonAndDot`, `spec.\.\:dotAndColon`),
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -374,6 +393,32 @@ func Test_traverseToFindEmptyFieldNeededModify(t *testing.T) {
 				fieldOfEmptySliceToDelete: sets.New[string]("spec.0.dd.ff"),
 			},
 		},
+		{
+			name: "traverse to find empty field needed modify with special characters in keys",
+			args: args{
+				root: gjson.Parse(`{
+					"spec": {
+						"replicas": 3,
+						":colons": [],
+						".dots": [],
+						":.colonAndDot": [],
+						".:dotAndColon": []
+					}
+				}`),
+				fieldPath:               nil,
+				fieldPathWithArrayIndex: nil,
+				fieldOfEmptySlice:       sets.New[string](`spec.\.dots`),
+				fieldOfEmptyStruct:      sets.New[string](`spec.\:colons`, `spec.\:\.colonAndDot`, `spec.\.\:dotAndColon`),
+			},
+			wants: wants{
+				fieldOfEmptySliceToStruct: sets.New[string](
+					`spec.\:colons`,
+					`spec.\:\.colonAndDot`,
+					`spec.\.\:dotAndColon`,
+				),
+				fieldOfEmptySliceToDelete: sets.New[string](),
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -435,6 +480,34 @@ func Test_convertEmptyObjectBackToEmptySlice(t *testing.T) {
 			wantErr: false,
 			want:    []byte(`[]`),
 		},
+		{
+			name: "convert empty object back to empty slice with special characters in keys",
+			args: args{
+				objBytes: []byte(`{"spec": {"replicas": 3, ":colons": [], ".dots": [], ":.colonAndDot": [], ".:dotAndColon": []}}`),
+				isStruct: true,
+				references: func() []*unstructured.Unstructured {
+					desired := &unstructured.Unstructured{}
+					observed := &unstructured.Unstructured{}
+					_ = json.Unmarshal([]byte(`{
+						"spec": {
+							"replicas": 3,
+							":colons": {},
+							":.colonAndDot": {},
+							".:dotAndColon": {}
+						}
+					}`), desired)
+					_ = json.Unmarshal([]byte(`{
+						"spec": {
+							"replicas": 3,
+							".dots": []
+						}
+					}`), observed)
+					return []*unstructured.Unstructured{desired, observed}
+				}(),
+			},
+			wantErr: false,
+			want:    []byte(`{"spec": {"replicas": 3, ":colons": {}, ".dots": [], ":.colonAndDot": {}, ".:dotAndColon": {}}}`),
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {