From 11b93fb41d324ee65936f40ef80bc833e55323aa Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Mon, 14 Apr 2025 02:05:10 +0300
Subject: [PATCH 1/6] feat(openapi-fetch): add transform options for response
 data handling

---
 packages/openapi-fetch/src/index.d.ts         | 15 +++
 packages/openapi-fetch/src/index.js           | 11 ++-
 packages/openapi-fetch/test/redocly.yaml      |  4 +
 .../test/transform/schemas/transform.d.ts     | 68 +++++++++++++
 .../test/transform/schemas/transform.yaml     | 96 +++++++++++++++++++
 .../test/transform/transform.test.ts          | 45 +++++++++
 6 files changed, 238 insertions(+), 1 deletion(-)
 create mode 100644 packages/openapi-fetch/test/transform/schemas/transform.d.ts
 create mode 100644 packages/openapi-fetch/test/transform/schemas/transform.yaml
 create mode 100644 packages/openapi-fetch/test/transform/transform.test.ts

diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts
index 79dec3d77..f69a3c32c 100644
--- a/packages/openapi-fetch/src/index.d.ts
+++ b/packages/openapi-fetch/src/index.d.ts
@@ -23,6 +23,8 @@ export interface ClientOptions extends Omit<RequestInit, "headers"> {
   querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions;
   /** global bodySerializer */
   bodySerializer?: BodySerializer<unknown>;
+  /** transform functions for request/response data */
+  transform?: TransformOptions<unknown, unknown>;
   headers?: HeadersOptions;
   /** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
   requestInitExt?: Record<string, unknown>;
@@ -64,6 +66,18 @@ export type QuerySerializerOptions = {
 
 export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;
 
+export type TransformOptions<T = any, R = any> = {
+  response?: (method: string, path: string, data: T) => R;
+};
+
+export type TransformFunction<T = any, R = any> = (
+  method: string,
+  path: string,
+  options: {
+    data: T;
+  },
+) => R;
+
 type BodyType<T = unknown> = {
   json: T;
   text: Awaited<ReturnType<Response["text"]>>;
@@ -127,6 +141,7 @@ export type MergedOptions<T = unknown> = {
   parseAs: ParseAs;
   querySerializer: QuerySerializer<T>;
   bodySerializer: BodySerializer<T>;
+  transform?: TransformOptions<T, T>;
   fetch: typeof globalThis.fetch;
 };
 
diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js
index ad83112e0..d7a048ef6 100644
--- a/packages/openapi-fetch/src/index.js
+++ b/packages/openapi-fetch/src/index.js
@@ -28,6 +28,7 @@ export default function createClient(clientOptions) {
     fetch: baseFetch = globalThis.fetch,
     querySerializer: globalQuerySerializer,
     bodySerializer: globalBodySerializer,
+    transform: globalTransform,
     headers: baseHeaders,
     requestInitExt = undefined,
     ...baseOptions
@@ -114,6 +115,7 @@ export default function createClient(clientOptions) {
         parseAs,
         querySerializer,
         bodySerializer,
+        transform: globalTransform,
       });
       for (const m of middlewares) {
         if (m && typeof m === "object" && typeof m.onRequest === "function") {
@@ -219,7 +221,14 @@ export default function createClient(clientOptions) {
       if (parseAs === "stream") {
         return { data: response.body, response };
       }
-      return { data: await response[parseAs](), response };
+
+      let responseData = await response[parseAs]();
+
+      if (globalTransform?.response && responseData !== undefined) {
+        responseData = globalTransform.response(request.method, schemaPath, responseData);
+      }
+
+      return { data: responseData, response };
     }
 
     // handle errors
diff --git a/packages/openapi-fetch/test/redocly.yaml b/packages/openapi-fetch/test/redocly.yaml
index 6030cfa41..efdf918bc 100644
--- a/packages/openapi-fetch/test/redocly.yaml
+++ b/packages/openapi-fetch/test/redocly.yaml
@@ -45,6 +45,10 @@ apis:
     root: ./path-based-client/schemas/path-based-client.yaml
     x-openapi-ts:
       output: ./path-based-client/schemas/path-based-client.d.ts
+  transform:
+    root: ./transform/schemas/transform.yaml
+    x-openapi-ts:
+      output: ./transform/schemas/transform.d.ts
   github:
     root: ../../openapi-typescript/examples/github-api.yaml
     x-openapi-ts:
diff --git a/packages/openapi-fetch/test/transform/schemas/transform.d.ts b/packages/openapi-fetch/test/transform/schemas/transform.d.ts
new file mode 100644
index 000000000..1f1413658
--- /dev/null
+++ b/packages/openapi-fetch/test/transform/schemas/transform.d.ts
@@ -0,0 +1,68 @@
+/**
+ * This file was manually created based on transform.yaml
+ */
+
+import type { PathsWithMethod } from "openapi-typescript-helpers";
+
+export interface paths {
+  "/posts": {
+    get: {
+      responses: {
+        200: {
+          content: {
+            "application/json": {
+              items: any[];
+              meta: {
+                total: number;
+              };
+            };
+          };
+        };
+      };
+    };
+    post: {
+      requestBody: {
+        content: {
+          "application/json": {
+            title: string;
+            content: string;
+          };
+        };
+      };
+      responses: {
+        200: {
+          content: {
+            "application/json": {
+              id: number;
+              name: string;
+              created_at: string;
+              updated_at: string;
+            };
+          };
+        };
+      };
+    };
+  };
+  "/posts/{id}": {
+    get: {
+      parameters: {
+        path: {
+          id: number;
+        };
+      };
+      responses: {
+        200: {
+          content: {
+            "application/json": {
+              id: number;
+              title: string;
+              content: string;
+              created_at: string;
+              updated_at: string;
+            };
+          };
+        };
+      };
+    };
+  };
+} 
\ No newline at end of file
diff --git a/packages/openapi-fetch/test/transform/schemas/transform.yaml b/packages/openapi-fetch/test/transform/schemas/transform.yaml
new file mode 100644
index 000000000..8e333411f
--- /dev/null
+++ b/packages/openapi-fetch/test/transform/schemas/transform.yaml
@@ -0,0 +1,96 @@
+openapi: 3.0.0
+info:
+  title: Transform Test API
+  version: 1.0.0
+paths:
+  /posts:
+    get:
+      summary: Get all posts
+      responses:
+        '200':
+          description: A list of posts
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  items:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        id:
+                          type: integer
+                        name:
+                          type: string
+                        description:
+                          type: string
+                        sensitive:
+                          type: string
+                        created_at:
+                          type: string
+                          format: date-time
+                  meta:
+                    type: object
+                    properties:
+                      total:
+                        type: integer
+    post:
+      summary: Create a new post
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                title:
+                  type: string
+                content:
+                  type: string
+      responses:
+        '200':
+          description: The created post
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  id:
+                    type: integer
+                  name:
+                    type: string
+                  created_at:
+                    type: string
+                    format: date-time
+                  updated_at:
+                    type: string
+                    format: date-time
+  /posts/{id}:
+    get:
+      summary: Get a post by ID
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema:
+            type: integer
+      responses:
+        '200':
+          description: A post
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  id:
+                    type: integer
+                  title:
+                    type: string
+                  content:
+                    type: string
+                  created_at:
+                    type: string
+                    format: date-time
+                  updated_at:
+                    type: string
+                    format: date-time 
\ No newline at end of file
diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts
new file mode 100644
index 000000000..cf6b8d2ea
--- /dev/null
+++ b/packages/openapi-fetch/test/transform/transform.test.ts
@@ -0,0 +1,45 @@
+import { assert, expect, test } from "vitest";
+import { createObservedClient } from "../helpers.js";
+import type { paths } from "./schemas/transform.js";
+
+interface PostResponse {
+  id: number;
+  title: string;
+  created_at: string | Date;
+}
+
+test("transforms date strings to Date objects", async () => {
+  const client = createObservedClient<paths>(
+    {
+      transform: {
+        response: (method, path, data) => {
+          if (!data || typeof data !== "object") return data;
+          
+          const result = { ...data } as PostResponse;
+          
+          if (typeof result.created_at === "string") {
+            result.created_at = new Date(result.created_at);
+          }
+          
+          return result;
+        }
+      }
+    },
+    async () => Response.json({ 
+      id: 1, 
+      title: "Test Post", 
+      created_at: "2023-01-01T00:00:00Z" 
+    })
+  );
+
+  const { data } = await client.GET("/posts/{id}", {
+    params: { path: { id: 1 } }
+  });
+
+  const post = data as PostResponse;
+
+  assert(post.created_at instanceof Date, "created_at should be a Date");
+  expect(post.created_at.getFullYear()).toBe(2023);
+  expect(post.created_at.getMonth()).toBe(0); // January
+  expect(post.created_at.getDate()).toBe(1);
+});

From 0400e5729471ac23704a838ff0085656392efc71 Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Mon, 14 Apr 2025 02:22:48 +0300
Subject: [PATCH 2/6] docs: add transform docs

---
 docs/openapi-fetch/api.md | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/docs/openapi-fetch/api.md b/docs/openapi-fetch/api.md
index 1a504c5d1..d60b9d8ef 100644
--- a/docs/openapi-fetch/api.md
+++ b/docs/openapi-fetch/api.md
@@ -19,6 +19,7 @@ createClient<paths>(options);
 | `fetch`           | `fetch`         | Fetch instance used for requests (default: `globalThis.fetch`)                                                                          |
 | `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer)                                                                                |
 | `bodySerializer`  | BodySerializer  | (optional) Provide a [bodySerializer](#bodyserializer)                                                                                  |
+| `transform`       | TransformOptions| (optional) Provide [transform functions](#transform) for response data                                                          |
 | (Fetch options)   |                 | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) |
 
 ## Fetch options
@@ -192,6 +193,24 @@ or when instantiating the client.
 
 :::
 
+## transform
+
+The transform option lets you modify request and response data before it's sent or after it's received. This is useful for tasks like deserialization.
+
+```ts
+const client = createClient<paths>({
+  transform: {
+    response: (method, path, data) => {
+      // Convert date strings to Date objects
+      if (data?.created_at) {
+        data.created_at = new Date(data.created_at);
+      }
+      return data;
+    }
+  }
+});
+```
+
 ## Path serialization
 
 openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://swagger.io/docs/specification/serialization/#path). This happens automatically, based on the specific format in your OpenAPI schema:

From ab55b09e1e9bfc58cd0400da3e68f7a312c8fc25 Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Mon, 14 Apr 2025 02:27:49 +0300
Subject: [PATCH 3/6] fix(transform.test): clean up whitespace and improve
 formatting in test case

---
 .../test/transform/transform.test.ts          | 23 ++++++++++---------
 1 file changed, 12 insertions(+), 11 deletions(-)

diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts
index cf6b8d2ea..b08b8b58f 100644
--- a/packages/openapi-fetch/test/transform/transform.test.ts
+++ b/packages/openapi-fetch/test/transform/transform.test.ts
@@ -14,26 +14,27 @@ test("transforms date strings to Date objects", async () => {
       transform: {
         response: (method, path, data) => {
           if (!data || typeof data !== "object") return data;
-          
+
           const result = { ...data } as PostResponse;
-          
+
           if (typeof result.created_at === "string") {
             result.created_at = new Date(result.created_at);
           }
-          
+
           return result;
-        }
-      }
+        },
+      },
     },
-    async () => Response.json({ 
-      id: 1, 
-      title: "Test Post", 
-      created_at: "2023-01-01T00:00:00Z" 
-    })
+    async () =>
+      Response.json({
+        id: 1,
+        title: "Test Post",
+        created_at: "2023-01-01T00:00:00Z",
+      }),
   );
 
   const { data } = await client.GET("/posts/{id}", {
-    params: { path: { id: 1 } }
+    params: { path: { id: 1 } },
   });
 
   const post = data as PostResponse;

From 973e7a92f3cc7d1ce3dc4921bff338f124fcee59 Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Mon, 14 Apr 2025 10:22:44 +0300
Subject: [PATCH 4/6] fix(transform.test): lint

---
 packages/openapi-fetch/test/transform/transform.test.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts
index b08b8b58f..354ced4f8 100644
--- a/packages/openapi-fetch/test/transform/transform.test.ts
+++ b/packages/openapi-fetch/test/transform/transform.test.ts
@@ -13,7 +13,9 @@ test("transforms date strings to Date objects", async () => {
     {
       transform: {
         response: (method, path, data) => {
-          if (!data || typeof data !== "object") return data;
+          if (!data || typeof data !== "object") {
+            return data
+          };
 
           const result = { ...data } as PostResponse;
 

From 82465d61e26ce3f508c9f63f70bc98aa5a8205da Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Mon, 14 Apr 2025 10:24:28 +0300
Subject: [PATCH 5/6] chore(changeset): add changeset for openapi-fetch
 transform option

---
 .changeset/strong-wombats-rhyme.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/strong-wombats-rhyme.md

diff --git a/.changeset/strong-wombats-rhyme.md b/.changeset/strong-wombats-rhyme.md
new file mode 100644
index 000000000..c09f3bfc9
--- /dev/null
+++ b/.changeset/strong-wombats-rhyme.md
@@ -0,0 +1,5 @@
+---
+"openapi-fetch": patch
+---
+
+openapi-fetch - add `transform` option (createClient) for response data handling

From 9321c34300f827c4f275eb7828fa31c8c7640349 Mon Sep 17 00:00:00 2001
From: Eliya Cohen <co.eliya2@gmail.com>
Date: Thu, 17 Apr 2025 13:47:17 +0300
Subject: [PATCH 6/6] fix(transform.test): correct linting issues by fixing
 whitespace in response transformation

---
 packages/openapi-fetch/test/transform/transform.test.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/openapi-fetch/test/transform/transform.test.ts b/packages/openapi-fetch/test/transform/transform.test.ts
index 354ced4f8..183a632fe 100644
--- a/packages/openapi-fetch/test/transform/transform.test.ts
+++ b/packages/openapi-fetch/test/transform/transform.test.ts
@@ -14,8 +14,8 @@ test("transforms date strings to Date objects", async () => {
       transform: {
         response: (method, path, data) => {
           if (!data || typeof data !== "object") {
-            return data
-          };
+            return data;
+          }
 
           const result = { ...data } as PostResponse;