Repository: RobinTail/express-zod-api
Pull request #677 (2022-10-29 18:05:41)

`IOSchema` chores
just moving the IOSchema to a dedicated file and adjusting its tests


Patch:
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index 6af954c9..fb7e55b4 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -7,62 +7,19 @@ import {
   LoggerConfig,
   loggerLevels,
 } from "./config-type";
-import { copyMeta, getMeta } from "./metadata";
+import { getMeta } from "./metadata";
 import { AuxMethod, Method } from "./method";
-import { AnyMiddlewareDef } from "./middleware";
 import { mimeMultipart } from "./mime";
 import { ZodUpload } from "./upload-schema";
 
 export type FlatObject = Record<string, any>;
 
-type Refined<T extends z.ZodType> = T extends z.ZodType<infer O>
-  ? z.ZodEffects<T, O, O>
-  : never;
-
-export type IOSchema<U extends UnknownKeysParam = any> =
-  | z.ZodObject<any, U>
-  | z.ZodUnion<[IOSchema<U>, ...IOSchema<U>[]]>
-  | z.ZodIntersection<IOSchema<U>, IOSchema<U>>
-  | z.ZodDiscriminatedUnion<string, z.Primitive, z.ZodObject<any, U>>
-  | Refined<z.ZodObject<any, U>>;
-
 export type ArrayElement<T extends readonly unknown[]> =
   T extends readonly (infer K)[] ? K : never;
 
 /** @see https://expressjs.com/en/guide/routing.html */
 export const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
 
-export type ProbableIntersection<
-  A extends IOSchema<"strip"> | null,
-  B extends IOSchema
-> = A extends null
-  ? B
-  : A extends IOSchema<"strip">
-  ? z.ZodIntersection<A, B>
-  : never;
-
-/**
- * @description intersects input schemas of middlewares and the endpoint
- * @since 07.03.2022 former combineEndpointAndMiddlewareInputSchemas()
- */
-export const getFinalEndpointInputSchema = <
-  MIN extends IOSchema<"strip"> | null,
-  IN extends IOSchema
->(
-  middlewares: AnyMiddlewareDef[],
-  input: IN
-): ProbableIntersection<MIN, IN> => {
-  const result = middlewares
-    .map(({ input: schema }) => schema)
-    .concat(input)
-    .reduce((acc, schema) => acc.and(schema)) as ProbableIntersection<MIN, IN>;
-  for (const middleware of middlewares) {
-    copyMeta(middleware.input, result);
-  }
-  copyMeta(input, result);
-  return result;
-};
-
 function areFilesAvailable(request: Request) {
   const contentType = request.header("content-type") || "";
   const isMultipart =
@@ -243,6 +200,3 @@ export type ErrMessage = Exclude<
 // the copy of the private Zod errorUtil.errToObj
 export const errToObj = (message: ErrMessage | undefined) =>
   typeof message === "string" ? { message } : message || {};
-
-// the copy of the private Zod utility type of ZodObject
-type UnknownKeysParam = "passthrough" | "strict" | "strip";
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 6485189f..5722eaae 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -6,11 +6,11 @@ import { CommonConfig } from "./config-type";
 import { ResultHandlerError } from "./errors";
 import {
   FlatObject,
-  IOSchema,
   getActualMethod,
   getInput,
   makeErrorFromAnything,
 } from "./common-helpers";
+import { IOSchema } from "./io-schema";
 import { LogicalContainer, combineContainers } from "./logical-container";
 import { AuxMethod, Method, MethodsDefinition } from "./method";
 import { AnyMiddlewareDef } from "./middleware";
diff --git a/src/endpoints-factory.ts b/src/endpoints-factory.ts
index b7fbc862..61d1b53c 100644
--- a/src/endpoints-factory.ts
+++ b/src/endpoints-factory.ts
@@ -1,15 +1,14 @@
 import { Request, Response } from "express";
 import { z } from "zod";
 import { ApiResponse } from "./api-response";
+import { FlatObject, hasUpload } from "./common-helpers";
+import { CommonConfig } from "./config-type";
+import { Endpoint, Handler } from "./endpoint";
 import {
-  FlatObject,
   IOSchema,
   ProbableIntersection,
   getFinalEndpointInputSchema,
-  hasUpload,
-} from "./common-helpers";
-import { CommonConfig } from "./config-type";
-import { Endpoint, Handler } from "./endpoint";
+} from "./io-schema";
 import { Method, MethodsDefinition } from "./method";
 import {
   AnyMiddlewareDef,
diff --git a/src/index.ts b/src/index.ts
index a809d40a..d81b6ee3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,7 +2,8 @@ export { createConfig, LoggerConfig } from "./config-type";
 export { AbstractEndpoint } from "./endpoint";
 export { Method } from "./method";
 export { EndpointsFactory, defaultEndpointsFactory } from "./endpoints-factory";
-export { IOSchema, FlatObject } from "./common-helpers";
+export { IOSchema } from "./io-schema";
+export { FlatObject } from "./common-helpers";
 export { createApiResponse } from "./api-response";
 export { createLogger } from "./logger";
 export { createMiddleware } from "./middleware";
diff --git a/src/io-schema.ts b/src/io-schema.ts
new file mode 100644
index 00000000..24194fd5
--- /dev/null
+++ b/src/io-schema.ts
@@ -0,0 +1,52 @@
+import { z } from "zod";
+import { copyMeta } from "./metadata";
+import { AnyMiddlewareDef } from "./middleware";
+
+// the copy of the private Zod utility type of ZodObject
+type UnknownKeysParam = "passthrough" | "strict" | "strip";
+
+type Refined<T extends z.ZodType> = T extends z.ZodType<infer O>
+  ? z.ZodEffects<T, O, O>
+  : never;
+
+/**
+ * @desc The type allowed on the top level of Middlewares and Endpoints
+ * @param U — only "strip" is allowed for Middlewares due to intersection issue (Zod) #600
+ * */
+export type IOSchema<U extends UnknownKeysParam = any> =
+  | z.ZodObject<any, U>
+  | z.ZodUnion<[IOSchema<U>, ...IOSchema<U>[]]>
+  | z.ZodIntersection<IOSchema<U>, IOSchema<U>>
+  | z.ZodDiscriminatedUnion<string, z.Primitive, z.ZodObject<any, U>>
+  | Refined<z.ZodObject<any, U>>;
+
+export type ProbableIntersection<
+  A extends IOSchema<"strip"> | null,
+  B extends IOSchema
+> = A extends null
+  ? B
+  : A extends IOSchema<"strip">
+  ? z.ZodIntersection<A, B>
+  : never;
+
+/**
+ * @description intersects input schemas of middlewares and the endpoint
+ * @since 07.03.2022 former combineEndpointAndMiddlewareInputSchemas()
+ */
+export const getFinalEndpointInputSchema = <
+  MIN extends IOSchema<"strip"> | null,
+  IN extends IOSchema
+>(
+  middlewares: AnyMiddlewareDef[],
+  input: IN
+): ProbableIntersection<MIN, IN> => {
+  const result = middlewares
+    .map(({ input: schema }) => schema)
+    .concat(input)
+    .reduce((acc, schema) => acc.and(schema)) as ProbableIntersection<MIN, IN>;
+  for (const middleware of middlewares) {
+    copyMeta(middleware.input, result);
+  }
+  copyMeta(input, result);
+  return result;
+};
diff --git a/src/middleware.ts b/src/middleware.ts
index 5952af35..91b66b10 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
 import { HttpError } from "http-errors";
 import { Logger } from "winston";
 import { z } from "zod";
-import { FlatObject, IOSchema } from "./common-helpers";
+import { FlatObject } from "./common-helpers";
+import { IOSchema } from "./io-schema";
 import { LogicalContainer } from "./logical-container";
 import { Security } from "./security";
 
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index bb3dffe7..fd1a0e43 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -16,7 +16,6 @@ import { omit } from "ramda";
 import { z } from "zod";
 import {
   ArrayElement,
-  IOSchema,
   getExamples,
   getRoutePathParams,
   routePathParamsRegex,
@@ -27,6 +26,7 @@ import { ZodDateOut, ZodDateOutDef } from "./date-out-schema";
 import { AbstractEndpoint } from "./endpoint";
 import { OpenAPIError } from "./errors";
 import { ZodFile, ZodFileDef } from "./file-schema";
+import { IOSchema } from "./io-schema";
 import {
   LogicalContainer,
   andToOr,
diff --git a/src/result-handler.ts b/src/result-handler.ts
index 645a613c..2bf582b4 100644
--- a/src/result-handler.ts
+++ b/src/result-handler.ts
@@ -3,11 +3,8 @@ import { Logger } from "winston";
 import { z } from "zod";
 import { ApiResponse, createApiResponse } from "./api-response";
 import { ResultHandlerError } from "./errors";
-import {
-  IOSchema,
-  getMessageFromError,
-  getStatusCodeFromError,
-} from "./common-helpers";
+import { getMessageFromError, getStatusCodeFromError } from "./common-helpers";
+import { IOSchema } from "./io-schema";
 import { getMeta, withMeta } from "./metadata";
 
 interface LastResortHandlerParams {


Test patch:
diff --git a/tests/unit/__snapshots__/common-helpers.spec.ts.snap b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
index 6a598e88..e89b0c61 100644
--- a/tests/unit/__snapshots__/common-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
@@ -27,256 +27,6 @@ exports[`Common Helpers defaultInputSources should be declared in a certain way
 }
 `;
 
-exports[`Common Helpers getFinalEndpointInputSchema() Should handle no middlewares 1`] = `
-{
-  "_type": "ZodObject",
-  "shape": {
-    "four": {
-      "_type": "ZodBoolean",
-    },
-  },
-}
-`;
-
-exports[`Common Helpers getFinalEndpointInputSchema() Should merge input object schemas 1`] = `
-{
-  "_type": "ZodIntersection",
-  "left": {
-    "_type": "ZodIntersection",
-    "left": {
-      "_type": "ZodIntersection",
-      "left": {
-        "_type": "ZodObject",
-        "shape": {
-          "one": {
-            "_type": "ZodString",
-          },
-        },
-      },
-      "right": {
-        "_type": "ZodObject",
-        "shape": {
-          "two": {
-            "_type": "ZodNumber",
-          },
-        },
-      },
-    },
-    "right": {
-      "_type": "ZodObject",
-      "shape": {
-        "three": {
-          "_type": "ZodNull",
-        },
-      },
-    },
-  },
-  "right": {
-    "_type": "ZodObject",
-    "shape": {
-      "four": {
-        "_type": "ZodBoolean",
-      },
-    },
-  },
-}
-`;
-
-exports[`Common Helpers getFinalEndpointInputSchema() Should merge intersection object schemas 1`] = `
-{
-  "_type": "ZodIntersection",
-  "left": {
-    "_type": "ZodIntersection",
-    "left": {
-      "_type": "ZodIntersection",
-      "left": {
-        "_type": "ZodObject",
-        "shape": {
-          "one": {
-            "_type": "ZodString",
-          },
-        },
-      },
-      "right": {
-        "_type": "ZodObject",
-        "shape": {
-          "two": {
-            "_type": "ZodNumber",
-          },
-        },
-      },
-    },
-    "right": {
-      "_type": "ZodIntersection",
-      "left": {
-        "_type": "ZodObject",
-        "shape": {
-          "three": {
-            "_type": "ZodNull",
-          },
-        },
-      },
-      "right": {
-        "_type": "ZodObject",
-        "shape": {
-          "four": {
-            "_type": "ZodBoolean",
-          },
-        },
-      },
-    },
-  },
-  "right": {
-    "_type": "ZodIntersection",
-    "left": {
-      "_type": "ZodObject",
-      "shape": {
-        "five": {
-          "_type": "ZodString",
-        },
-      },
-    },
-    "right": {
-      "_type": "ZodObject",
-      "shape": {
-        "six": {
-          "_type": "ZodNumber",
-        },
-      },
-    },
-  },
-}
-`;
-
-exports[`Common Helpers getFinalEndpointInputSchema() Should merge mixed object schemas 1`] = `
-{
-  "_type": "ZodIntersection",
-  "left": {
-    "_type": "ZodIntersection",
-    "left": {
-      "_type": "ZodIntersection",
-      "left": {
-        "_type": "ZodObject",
-        "shape": {
-          "one": {
-            "_type": "ZodString",
-          },
-        },
-      },
-      "right": {
-        "_type": "ZodObject",
-        "shape": {
-          "two": {
-            "_type": "ZodNumber",
-          },
-        },
-      },
-    },
-    "right": {
-      "_type": "ZodUnion",
-      "options": [
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "three": {
-              "_type": "ZodNull",
-            },
-          },
-        },
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "four": {
-              "_type": "ZodBoolean",
-            },
-          },
-        },
-      ],
-    },
-  },
-  "right": {
-    "_type": "ZodObject",
-    "shape": {
-      "five": {
-        "_type": "ZodString",
-      },
-    },
-  },
-}
-`;
-
-exports[`Common Helpers getFinalEndpointInputSchema() Should merge union object schemas 1`] = `
-{
-  "_type": "ZodIntersection",
-  "left": {
-    "_type": "ZodIntersection",
-    "left": {
-      "_type": "ZodUnion",
-      "options": [
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "one": {
-              "_type": "ZodString",
-            },
-          },
-        },
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "two": {
-              "_type": "ZodNumber",
-            },
-          },
-        },
-      ],
-    },
-    "right": {
-      "_type": "ZodUnion",
-      "options": [
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "three": {
-              "_type": "ZodNull",
-            },
-          },
-        },
-        {
-          "_type": "ZodObject",
-          "shape": {
-            "four": {
-              "_type": "ZodBoolean",
-            },
-          },
-        },
-      ],
-    },
-  },
-  "right": {
-    "_type": "ZodUnion",
-    "options": [
-      {
-        "_type": "ZodObject",
-        "shape": {
-          "five": {
-            "_type": "ZodString",
-          },
-        },
-      },
-      {
-        "_type": "ZodObject",
-        "shape": {
-          "six": {
-            "_type": "ZodNumber",
-          },
-        },
-      },
-    ],
-  },
-}
-`;
-
 exports[`Common Helpers getMessageFromError() should compile a string from ZodError 1`] = `"user/id: expected number, got string; user/name: expected string, got number"`;
 
 exports[`Common Helpers getMessageFromError() should handle empty path in ZodIssue 1`] = `"Top level refinement issue"`;
diff --git a/tests/unit/__snapshots__/io-schema.spec.ts.snap b/tests/unit/__snapshots__/io-schema.spec.ts.snap
new file mode 100644
index 00000000..de635842
--- /dev/null
+++ b/tests/unit/__snapshots__/io-schema.spec.ts.snap
@@ -0,0 +1,261 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Should handle no middlewares 1`] = `
+{
+  "_type": "ZodObject",
+  "shape": {
+    "four": {
+      "_type": "ZodBoolean",
+    },
+  },
+}
+`;
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Should merge input object schemas 1`] = `
+{
+  "_type": "ZodIntersection",
+  "left": {
+    "_type": "ZodIntersection",
+    "left": {
+      "_type": "ZodIntersection",
+      "left": {
+        "_type": "ZodObject",
+        "shape": {
+          "one": {
+            "_type": "ZodString",
+          },
+        },
+      },
+      "right": {
+        "_type": "ZodObject",
+        "shape": {
+          "two": {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    },
+    "right": {
+      "_type": "ZodObject",
+      "shape": {
+        "three": {
+          "_type": "ZodNull",
+        },
+      },
+    },
+  },
+  "right": {
+    "_type": "ZodObject",
+    "shape": {
+      "four": {
+        "_type": "ZodBoolean",
+      },
+    },
+  },
+}
+`;
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Should merge intersection object schemas 1`] = `
+{
+  "_type": "ZodIntersection",
+  "left": {
+    "_type": "ZodIntersection",
+    "left": {
+      "_type": "ZodIntersection",
+      "left": {
+        "_type": "ZodObject",
+        "shape": {
+          "one": {
+            "_type": "ZodString",
+          },
+        },
+      },
+      "right": {
+        "_type": "ZodObject",
+        "shape": {
+          "two": {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    },
+    "right": {
+      "_type": "ZodIntersection",
+      "left": {
+        "_type": "ZodObject",
+        "shape": {
+          "three": {
+            "_type": "ZodNull",
+          },
+        },
+      },
+      "right": {
+        "_type": "ZodObject",
+        "shape": {
+          "four": {
+            "_type": "ZodBoolean",
+          },
+        },
+      },
+    },
+  },
+  "right": {
+    "_type": "ZodIntersection",
+    "left": {
+      "_type": "ZodObject",
+      "shape": {
+        "five": {
+          "_type": "ZodString",
+        },
+      },
+    },
+    "right": {
+      "_type": "ZodObject",
+      "shape": {
+        "six": {
+          "_type": "ZodNumber",
+        },
+      },
+    },
+  },
+}
+`;
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Should merge mixed object schemas 1`] = `
+{
+  "_type": "ZodIntersection",
+  "left": {
+    "_type": "ZodIntersection",
+    "left": {
+      "_type": "ZodIntersection",
+      "left": {
+        "_type": "ZodObject",
+        "shape": {
+          "one": {
+            "_type": "ZodString",
+          },
+        },
+      },
+      "right": {
+        "_type": "ZodObject",
+        "shape": {
+          "two": {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    },
+    "right": {
+      "_type": "ZodUnion",
+      "options": [
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "three": {
+              "_type": "ZodNull",
+            },
+          },
+        },
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "four": {
+              "_type": "ZodBoolean",
+            },
+          },
+        },
+      ],
+    },
+  },
+  "right": {
+    "_type": "ZodObject",
+    "shape": {
+      "five": {
+        "_type": "ZodString",
+      },
+    },
+  },
+}
+`;
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Should merge union object schemas 1`] = `
+{
+  "_type": "ZodIntersection",
+  "left": {
+    "_type": "ZodIntersection",
+    "left": {
+      "_type": "ZodUnion",
+      "options": [
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "one": {
+              "_type": "ZodString",
+            },
+          },
+        },
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "two": {
+              "_type": "ZodNumber",
+            },
+          },
+        },
+      ],
+    },
+    "right": {
+      "_type": "ZodUnion",
+      "options": [
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "three": {
+              "_type": "ZodNull",
+            },
+          },
+        },
+        {
+          "_type": "ZodObject",
+          "shape": {
+            "four": {
+              "_type": "ZodBoolean",
+            },
+          },
+        },
+      ],
+    },
+  },
+  "right": {
+    "_type": "ZodUnion",
+    "options": [
+      {
+        "_type": "ZodObject",
+        "shape": {
+          "five": {
+            "_type": "ZodString",
+          },
+        },
+      },
+      {
+        "_type": "ZodObject",
+        "shape": {
+          "six": {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    ],
+  },
+}
+`;
+
+exports[`I/O Schema and related helpers getFinalEndpointInputSchema() Zod Issue #600: can not intersect object schema with passthrough and transformation 1`] = `
+[ZodError: [
+  {
+    "code": "invalid_intersection_types",
+    "path": [],
+    "message": "Intersection results could not be merged"
+  }
+]]
+`;
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index 3a8c0b71..4d9fa0e2 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -1,11 +1,9 @@
 import { UploadedFile } from "express-fileupload";
-import { expectNotType, expectType } from "tsd";
+import { expectType } from "tsd";
 import {
-  IOSchema,
   combinations,
   defaultInputSources,
   getExamples,
-  getFinalEndpointInputSchema,
   getInput,
   getMessageFromError,
   getRoutePathParams,
@@ -15,271 +13,10 @@ import {
   isValidDate,
   makeErrorFromAnything,
 } from "../../src/common-helpers";
-import { createHttpError, createMiddleware, withMeta, z } from "../../src";
+import { createHttpError, withMeta, z } from "../../src";
 import { Request } from "express";
-import { getMeta } from "../../src/metadata";
-import { AnyMiddlewareDef } from "../../src/middleware";
-import { serializeSchemaForTest } from "../helpers";
 
 describe("Common Helpers", () => {
-  describe("IOSchema", () => {
-    test("accepts object", () => {
-      expectType<IOSchema>(z.object({}));
-      expectType<IOSchema<"strip">>(z.object({}));
-      expectType<IOSchema<"strict">>(z.object({}).strict());
-      expectType<IOSchema<"passthrough">>(z.object({}).passthrough());
-      expectType<IOSchema<"strip">>(z.object({}).strip());
-      expectNotType<IOSchema<"passthrough">>(z.object({}));
-    });
-    test("accepts union of objects", () => {
-      expectType<IOSchema>(z.union([z.object({}), z.object({})]));
-      expectType<IOSchema>(z.object({}).or(z.object({})));
-      expectType<IOSchema>(z.object({}).or(z.object({}).or(z.object({}))));
-      expectNotType<IOSchema>(z.object({}).or(z.string()));
-    });
-    test("accepts intersection of objects", () => {
-      expectType<IOSchema>(z.intersection(z.object({}), z.object({})));
-      expectType<IOSchema>(z.object({}).and(z.object({})));
-      expectType<IOSchema>(z.object({}).and(z.object({}).and(z.object({}))));
-      expectNotType<IOSchema>(z.object({}).and(z.string()));
-    });
-    test("accepts discriminated union of objects", () => {
-      expectType<IOSchema>(
-        z.discriminatedUnion("type", [
-          z.object({ type: z.literal("one") }),
-          z.object({ type: z.literal("two") }),
-        ])
-      );
-    });
-    test("accepts a mix of types based on object", () => {
-      expectType<IOSchema>(z.object({}).or(z.object({}).and(z.object({}))));
-      expectType<IOSchema>(z.object({}).and(z.object({}).or(z.object({}))));
-    });
-    test("accepts a refinement of object", () => {
-      expectType<IOSchema>(z.object({}).refine(() => true));
-      expectType<IOSchema>(
-        z.object({}).refine((obj) => ({ ...obj, test: true }))
-      );
-      expectNotType<IOSchema>(
-        z.object({}).transform((obj) => Object.keys(obj))
-      );
-    });
-  });
-
-  describe("getFinalEndpointInputSchema()", () => {
-    test("Should handle no middlewares", () => {
-      const middlewares: AnyMiddlewareDef[] = [];
-      const endpointInput = z.object({
-        four: z.boolean(),
-      });
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(result).toBeInstanceOf(z.ZodObject);
-      expect(serializeSchemaForTest(result)).toMatchSnapshot();
-    });
-
-    test("Should merge input object schemas", () => {
-      const middlewares: AnyMiddlewareDef[] = [
-        createMiddleware({
-          input: z.object({
-            one: z.string(),
-          }),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: z.object({
-            two: z.number(),
-          }),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: z.object({
-            three: z.null(),
-          }),
-          middleware: jest.fn(),
-        }),
-      ];
-      const endpointInput = z.object({
-        four: z.boolean(),
-      });
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(result).toBeInstanceOf(z.ZodIntersection);
-      expect(serializeSchemaForTest(result)).toMatchSnapshot();
-    });
-
-    test("Should merge union object schemas", () => {
-      const middlewares: AnyMiddlewareDef[] = [
-        createMiddleware({
-          input: z
-            .object({
-              one: z.string(),
-            })
-            .or(
-              z.object({
-                two: z.number(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: z
-            .object({
-              three: z.null(),
-            })
-            .or(
-              z.object({
-                four: z.boolean(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-      ];
-      const endpointInput = z
-        .object({
-          five: z.string(),
-        })
-        .or(
-          z.object({
-            six: z.number(),
-          })
-        );
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(result).toBeInstanceOf(z.ZodIntersection);
-      expect(serializeSchemaForTest(result)).toMatchSnapshot();
-    });
-
-    test("Should merge intersection object schemas", () => {
-      const middlewares: AnyMiddlewareDef[] = [
-        createMiddleware({
-          input: z
-            .object({
-              one: z.string(),
-            })
-            .and(
-              z.object({
-                two: z.number(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: z
-            .object({
-              three: z.null(),
-            })
-            .and(
-              z.object({
-                four: z.boolean(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-      ];
-      const endpointInput = z
-        .object({
-          five: z.string(),
-        })
-        .and(
-          z.object({
-            six: z.number(),
-          })
-        );
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(result).toBeInstanceOf(z.ZodIntersection);
-      expect(serializeSchemaForTest(result)).toMatchSnapshot();
-    });
-
-    test("Should merge mixed object schemas", () => {
-      const middlewares: AnyMiddlewareDef[] = [
-        createMiddleware({
-          input: z
-            .object({
-              one: z.string(),
-            })
-            .and(
-              z.object({
-                two: z.number(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: z
-            .object({
-              three: z.null(),
-            })
-            .or(
-              z.object({
-                four: z.boolean(),
-              })
-            ),
-          middleware: jest.fn(),
-        }),
-      ];
-      const endpointInput = z.object({
-        five: z.string(),
-      });
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(result).toBeInstanceOf(z.ZodIntersection);
-      expect(serializeSchemaForTest(result)).toMatchSnapshot();
-    });
-
-    test("Should merge examples in case of using withMeta()", () => {
-      const middlewares: AnyMiddlewareDef[] = [
-        createMiddleware({
-          input: withMeta(
-            z
-              .object({
-                one: z.string(),
-              })
-              .and(
-                z.object({
-                  two: z.number(),
-                })
-              )
-          ).example({
-            one: "test",
-            two: 123,
-          }),
-          middleware: jest.fn(),
-        }),
-        createMiddleware({
-          input: withMeta(
-            z
-              .object({
-                three: z.null(),
-              })
-              .or(
-                z.object({
-                  four: z.boolean(),
-                })
-              )
-          ).example({
-            three: null,
-            four: true,
-          }),
-          middleware: jest.fn(),
-        }),
-      ];
-      const endpointInput = withMeta(
-        z.object({
-          five: z.string(),
-        })
-      ).example({
-        five: "some",
-      });
-      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
-      expect(getMeta(result, "examples")).toEqual([
-        {
-          one: "test",
-          two: 123,
-          three: null,
-          four: true,
-          five: "some",
-        },
-      ]);
-    });
-  });
-
   describe("defaultInputSources", () => {
     test("should be declared in a certain way", () => {
       expect(defaultInputSources).toMatchSnapshot();
diff --git a/tests/unit/io-schema.spec.ts b/tests/unit/io-schema.spec.ts
new file mode 100644
index 00000000..092f8d56
--- /dev/null
+++ b/tests/unit/io-schema.spec.ts
@@ -0,0 +1,291 @@
+import { expectNotType, expectType } from "tsd";
+import { IOSchema, createMiddleware, withMeta, z } from "../../src";
+import { getFinalEndpointInputSchema } from "../../src/io-schema";
+import { getMeta } from "../../src/metadata";
+import { AnyMiddlewareDef } from "../../src/middleware";
+import { serializeSchemaForTest } from "../helpers";
+
+describe("I/O Schema and related helpers", () => {
+  describe("IOSchema", () => {
+    test("accepts object", () => {
+      expectType<IOSchema>(z.object({}));
+      expectType<IOSchema<"strip">>(z.object({}));
+      expectType<IOSchema<"strict">>(z.object({}).strict());
+      expectType<IOSchema<"passthrough">>(z.object({}).passthrough());
+      expectType<IOSchema<"strip">>(z.object({}).strip());
+    });
+    test("respects the UnknownKeys type argument", () => {
+      expectNotType<IOSchema<"passthrough">>(z.object({}));
+    });
+    test("accepts union of objects", () => {
+      expectType<IOSchema>(z.union([z.object({}), z.object({})]));
+      expectType<IOSchema>(z.object({}).or(z.object({})));
+      expectType<IOSchema>(z.object({}).or(z.object({}).or(z.object({}))));
+    });
+    test("does not accept union of object and array of objects", () => {
+      expectNotType<IOSchema>(z.object({}).or(z.array(z.object({}))));
+    });
+    test("accepts intersection of objects", () => {
+      expectType<IOSchema>(z.intersection(z.object({}), z.object({})));
+      expectType<IOSchema>(z.object({}).and(z.object({})));
+      expectType<IOSchema>(z.object({}).and(z.object({}).and(z.object({}))));
+    });
+    test("does not accepts intersection of object with array of objects", () => {
+      expectNotType<IOSchema>(z.object({}).and(z.array(z.object({}))));
+    });
+    test("accepts discriminated union of objects", () => {
+      expectType<IOSchema>(
+        z.discriminatedUnion("type", [
+          z.object({ type: z.literal("one") }),
+          z.object({ type: z.literal("two") }),
+        ])
+      );
+    });
+    test("accepts a mix of types based on object", () => {
+      expectType<IOSchema>(z.object({}).or(z.object({}).and(z.object({}))));
+      expectType<IOSchema>(z.object({}).and(z.object({}).or(z.object({}))));
+    });
+    test("accepts a refinement of object", () => {
+      expectType<IOSchema>(z.object({}).refine(() => true));
+      expectType<IOSchema>(z.object({}).superRefine(() => true));
+      expectType<IOSchema>(
+        z.object({}).refinement(() => true, {
+          code: "custom",
+          message: "test",
+        })
+      );
+    });
+    test("does not accept transformation of object", () => {
+      expectNotType<IOSchema>(z.object({}).transform(() => true));
+    });
+  });
+
+  describe("getFinalEndpointInputSchema()", () => {
+    test("Should handle no middlewares", () => {
+      const middlewares: AnyMiddlewareDef[] = [];
+      const endpointInput = z.object({
+        four: z.boolean(),
+      });
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodObject);
+      expect(serializeSchemaForTest(result)).toMatchSnapshot();
+    });
+
+    test("Should merge input object schemas", () => {
+      const middlewares: AnyMiddlewareDef[] = [
+        createMiddleware({
+          input: z.object({
+            one: z.string(),
+          }),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: z.object({
+            two: z.number(),
+          }),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: z.object({
+            three: z.null(),
+          }),
+          middleware: jest.fn(),
+        }),
+      ];
+      const endpointInput = z.object({
+        four: z.boolean(),
+      });
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
+      expect(serializeSchemaForTest(result)).toMatchSnapshot();
+    });
+
+    test("Should merge union object schemas", () => {
+      const middlewares: AnyMiddlewareDef[] = [
+        createMiddleware({
+          input: z
+            .object({
+              one: z.string(),
+            })
+            .or(
+              z.object({
+                two: z.number(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: z
+            .object({
+              three: z.null(),
+            })
+            .or(
+              z.object({
+                four: z.boolean(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+      ];
+      const endpointInput = z
+        .object({
+          five: z.string(),
+        })
+        .or(
+          z.object({
+            six: z.number(),
+          })
+        );
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
+      expect(serializeSchemaForTest(result)).toMatchSnapshot();
+    });
+
+    test("Should merge intersection object schemas", () => {
+      const middlewares: AnyMiddlewareDef[] = [
+        createMiddleware({
+          input: z
+            .object({
+              one: z.string(),
+            })
+            .and(
+              z.object({
+                two: z.number(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: z
+            .object({
+              three: z.null(),
+            })
+            .and(
+              z.object({
+                four: z.boolean(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+      ];
+      const endpointInput = z
+        .object({
+          five: z.string(),
+        })
+        .and(
+          z.object({
+            six: z.number(),
+          })
+        );
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
+      expect(serializeSchemaForTest(result)).toMatchSnapshot();
+    });
+
+    test("Zod Issue #600: can not intersect object schema with passthrough and transformation", () => {
+      // @see https://github.com/colinhacks/zod/issues/600
+      // this is the reason why IOSchema is generic and middlewares have to be "strip"
+      const left = z.object({}).passthrough();
+      const right = z.object({
+        id: z.string().transform((str) => parseInt(str)),
+      });
+      const schema = z.intersection(left, right);
+      const result = schema.safeParse({
+        id: "123",
+      });
+      expect(result.success).toBeFalsy();
+      expect((result as z.SafeParseError<any>).error).toMatchSnapshot();
+    });
+
+    test("Should merge mixed object schemas", () => {
+      const middlewares: AnyMiddlewareDef[] = [
+        createMiddleware({
+          input: z
+            .object({
+              one: z.string(),
+            })
+            .and(
+              z.object({
+                two: z.number(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: z
+            .object({
+              three: z.null(),
+            })
+            .or(
+              z.object({
+                four: z.boolean(),
+              })
+            ),
+          middleware: jest.fn(),
+        }),
+      ];
+      const endpointInput = z.object({
+        five: z.string(),
+      });
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
+      expect(serializeSchemaForTest(result)).toMatchSnapshot();
+    });
+
+    test("Should merge examples in case of using withMeta()", () => {
+      const middlewares: AnyMiddlewareDef[] = [
+        createMiddleware({
+          input: withMeta(
+            z
+              .object({
+                one: z.string(),
+              })
+              .and(
+                z.object({
+                  two: z.number(),
+                })
+              )
+          ).example({
+            one: "test",
+            two: 123,
+          }),
+          middleware: jest.fn(),
+        }),
+        createMiddleware({
+          input: withMeta(
+            z
+              .object({
+                three: z.null(),
+              })
+              .or(
+                z.object({
+                  four: z.boolean(),
+                })
+              )
+          ).example({
+            three: null,
+            four: true,
+          }),
+          middleware: jest.fn(),
+        }),
+      ];
+      const endpointInput = withMeta(
+        z.object({
+          five: z.string(),
+        })
+      ).example({
+        five: "some",
+      });
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(getMeta(result, "examples")).toEqual([
+        {
+          one: "test",
+          two: 123,
+          three: null,
+          four: true,
+          five: "some",
+        },
+      ]);
+    });
+  });
+});