Repository: kubernetes-client/javascript (ts)

Issue:
NetworkPolicy.spec.ingress.from is not correctly serialized
**Describe the bug**


A network policy resource is correctly deserialized when read from the cluster.
The ingress spec is correctly converted from `from` to `_from`.  See https://github.com/kubernetes-client/javascript/blob/main/src/gen/models/V1NetworkPolicyIngressRule.ts#L24

But the serialization (e.g. when using with `client.replace`) does not correctly set `from` field. see the snippet below.

**Client Version**
e.g. `1.1.2`

**To Reproduce**
 See example

**Expected behavior**

Resource is correctly serialized

**Example Code**

```
const netPol: V1 NetworkPolicy = {
        apiVersion: 'networking.k8s.io/v1',
        kind: 'NetworkPolicy',
        metadata: {
            name: 'test',
            namespace: 'default',
        },
        spec: {
            podSelector: {
                matchLabels: {
                    app: 'workspace',
                },
            },
            policyTypes: ['Ingress', 'Egress'],
            ingress: [
                {
                    _from: [{
                        podSelector: {matchLabels: {app: 'foo'}},
                    }],
                    ports: [{port: 123}],
                },
            ],
            egress: [
                {
                    to: [{
                        matchLabels: {app: 'bar'},
                    }],
                },
            ],
        },
    };
    client.create(netPol); // does not include the spec.ingress.from fields
```

**Environment (please complete the following information):**

- OS: Linux
- Node.js version: 22


Pull request description:
fix(object): properly serialize objects on create/replace/patch
Fixes #2396 

Patch:
diff --git a/package-lock.json b/package-lock.json
index 10beef9f..ef67444f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3610,9 +3610,9 @@
             "license": "0BSD"
         },
         "node_modules/tsx": {
-            "version": "4.19.4",
-            "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
-            "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
+            "version": "4.19.3",
+            "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
+            "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
             "dev": true,
             "license": "MIT",
             "dependencies": {
diff --git a/src/object.ts b/src/object.ts
index 52c98a7d..0548b03a 100644
--- a/src/object.ts
+++ b/src/object.ts
@@ -107,12 +107,13 @@ export class KubernetesObjectApi {
         if (fieldManager !== undefined) {
             requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
         }
+        const type = await this.getSerializationType(spec.apiVersion, spec.kind);
 
         // Body Params
         const contentType = ObjectSerializer.getPreferredMediaType([]);
         requestContext.setHeaderParam('Content-Type', contentType);
         const serializedBody = ObjectSerializer.stringify(
-            ObjectSerializer.serialize(spec, 'any'),
+            ObjectSerializer.serialize(spec, type),
             contentType,
         );
         requestContext.setBody(serializedBody);
@@ -268,9 +269,11 @@ export class KubernetesObjectApi {
             requestContext.setQueryParam('force', ObjectSerializer.serialize(force, 'boolean'));
         }
 
+        const type = await this.getSerializationType(spec.apiVersion, spec.kind);
+
         // Body Params
         const serializedBody = ObjectSerializer.stringify(
-            ObjectSerializer.serialize(spec, 'any'),
+            ObjectSerializer.serialize(spec, type),
             // TODO: use the patch content type once ObjectSerializer supports it.
             'application/json',
         );
@@ -465,11 +468,13 @@ export class KubernetesObjectApi {
             requestContext.setQueryParam('fieldManager', ObjectSerializer.serialize(fieldManager, 'string'));
         }
 
+        const type = await this.getSerializationType(spec.apiVersion, spec.kind);
+
         // Body Params
         const contentType = ObjectSerializer.getPreferredMediaType([]);
         requestContext.setHeaderParam('Content-Type', contentType);
         const serializedBody = ObjectSerializer.stringify(
-            ObjectSerializer.serialize(spec, 'any'),
+            ObjectSerializer.serialize(spec, type),
             contentType,
         );
         requestContext.setBody(serializedBody);


Test patch:
diff --git a/src/object_test.ts b/src/object_test.ts
index 2ccb298d..e27f77ed 100644
--- a/src/object_test.ts
+++ b/src/object_test.ts
@@ -1071,6 +1071,9 @@ describe('KubernetesObject', () => {
             kc.loadFromOptions(testConfigOptions);
             client = KubernetesObjectApi.makeApiClient(kc);
             (client as any).apiVersionResourceCache.v1 = JSON.parse(resourceBodies.core);
+            (client as any).apiVersionResourceCache['networking.k8s.io/v1'] = JSON.parse(
+                resourceBodies.networking,
+            );
         });
 
         it('should modify resources with defaults', async () => {
@@ -1520,6 +1523,125 @@ describe('KubernetesObject', () => {
             }
         });
 
+        it('should properly serialize resources on modify', async () => {
+            const netPol = {
+                apiVersion: 'networking.k8s.io/v1',
+                kind: 'NetworkPolicy',
+                metadata: {
+                    name: 'k8s-js-client-test',
+                    namespace: 'default',
+                },
+                spec: {
+                    podSelector: {
+                        matchLabels: {
+                            app: 'my-app',
+                        },
+                    },
+                    policyTypes: ['Ingress'],
+                    ingress: [
+                        {
+                            _from: [
+                                {
+                                    podSelector: { matchLabels: { app: 'foo' } },
+                                },
+                            ],
+                            ports: [{ port: 123 }],
+                        },
+                    ],
+                },
+            };
+            const serializedNetPol = {
+                apiVersion: 'networking.k8s.io/v1',
+                kind: 'NetworkPolicy',
+                metadata: {
+                    name: 'k8s-js-client-test',
+                    namespace: 'default',
+                },
+                spec: {
+                    podSelector: {
+                        matchLabels: {
+                            app: 'my-app',
+                        },
+                    },
+                    policyTypes: ['Ingress'],
+                    ingress: [
+                        {
+                            from: [
+                                {
+                                    podSelector: { matchLabels: { app: 'foo' } },
+                                },
+                            ],
+                            ports: [{ port: 123 }],
+                        },
+                    ],
+                },
+            };
+            const returnBody = `{
+  "kind": "NetworkPolicy",
+  "apiVersion": "networking.k8s.io/v1",
+  "metadata": {
+    "name": "k8s-js-client-test",
+    "namespace": "default",
+    "selfLink": "/api/v1/namespaces/default/services/k8s-js-client-test",
+    "uid": "6a43eddc-26bf-424e-ab30-cde3041a706a",
+    "resourceVersion": "32373",
+    "creationTimestamp": "2020-05-11T17:34:25Z"
+  },
+  "spec": {
+    "policyTypes": ["Ingress"],
+    "podSelector": {
+      "matchLabels": {
+        "app": "my-app"
+      }
+    },
+    "ingress": [
+      {
+        "from": [{
+          "podSelector": {
+            "matchLabels": {
+                "app": "foo"
+            }
+          }
+        }],
+        "ports": [{"port": 123}]
+      }
+    ]
+  }
+}`;
+            const methods = [
+                {
+                    m: client.create,
+                    v: 'POST',
+                    p: '/apis/networking.k8s.io/v1/namespaces/default/networkpolicies',
+                    c: 201,
+                    b: returnBody,
+                },
+                {
+                    m: client.replace,
+                    v: 'PUT',
+                    p: '/apis/networking.k8s.io/v1/namespaces/default/networkpolicies/k8s-js-client-test',
+                    c: 200,
+                    b: returnBody,
+                },
+                {
+                    m: client.patch,
+                    v: 'PATCH',
+                    p: '/apis/networking.k8s.io/v1/namespaces/default/networkpolicies/k8s-js-client-test',
+                    c: 200,
+                    b: returnBody,
+                },
+            ];
+            for (const m of methods) {
+                const scope = nock('https://d.i.y')
+                    .intercept(m.p, m.v, serializedNetPol)
+                    .reply(m.c, m.b, contentTypeJsonHeader);
+                // TODO: Figure out why Typescript barfs if we do m.call
+                const hack_m = m.m as any;
+                await hack_m.call(client, netPol);
+                scope.done();
+            }
+        });
+
         it('should replace a resource', async () => {
             const s = {
                 apiVersion: 'v1',