Repository: RobinTail/express-zod-api
Pull request #10 (2021-05-08 21:25:19)

Intersection and Union as I/O


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..4111c1ca
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,71 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ master ]
+  schedule:
+    - cron: '26 8 * * 1'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'javascript' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+        # Learn more:
+        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b4cda9f..2c398503 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,37 @@
 
 ## Version 0
 
+### v0.7.0
+- Zod version is v3.0.0-beta.1
+- Ability to use z.ZodIntersection and z.ZodUnion as an I/O schema for handlers and middlewares.
+```typescript
+// example
+const middleware = createMiddleware({
+  input: z.object({
+    one: z.string()
+  }).or(z.object({
+    two: z.number()
+  })),
+  middleware: async ({input}) => ({
+    input // => type: { one: string } | { two: number }
+  })
+});
+```
+- Ability to use `z.transform()` in handler's output schema.
+```typescript
+// example
+const endpoint = factory.build({
+  methods: ['post'],
+  input: z.object({}).nonstrict(),
+  output: z.object({
+    value: z.string().transform((str) => str.length)
+  }),
+  handler: async ({input, options}) => ({
+    transform: 'test' // => in response: { transform: 4 }
+  })
+});
+```
+
 ### v0.6.1
 - Nothing special. Just new logo and the dataflow diagram update.
 
diff --git a/src/endpoint.ts b/src/endpoint.ts
index ec3fc81a..05a24082 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -1,7 +1,7 @@
 import {Logger} from 'winston';
 import {z} from 'zod';
 import {ConfigType} from './config-type';
-import {combineEndpointAndMiddlewareInputSchemas, getInitialInput, Merge, ObjectSchema} from './helpers';
+import {combineEndpointAndMiddlewareInputSchemas, getInitialInput, IOSchema, Merge} from './helpers';
 import {Request, Response} from 'express';
 import {MiddlewareDefinition} from './middleware';
 import {defaultResultHandler, ResultHandler} from './result-handler';
@@ -26,8 +26,8 @@ export abstract class AbstractEndpoint {
     return this.methods;
   }
 
-  abstract getInputSchema(): ObjectSchema;
-  abstract getOutputSchema(): ObjectSchema;
+  abstract getInputSchema(): IOSchema;
+  abstract getOutputSchema(): IOSchema;
 }
 
 export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch';
@@ -37,7 +37,7 @@ export type EndpointInput<T> = T extends Endpoint<infer IN, any, infer mIN, any>
 export type EndpointOutput<T> = T extends Endpoint<any, infer OUT, any, any> ? z.output<OUT> : never;
 
 /** mIN, OPT - from Middlewares */
-export class Endpoint<IN extends ObjectSchema, OUT extends ObjectSchema, mIN, OPT> extends AbstractEndpoint {
+export class Endpoint<IN extends IOSchema, OUT extends IOSchema, mIN, OPT> extends AbstractEndpoint {
   protected middlewares: MiddlewareDefinition<any, any, any>[] = [];
   protected inputSchema: Merge<IN, mIN>; // combined with middlewares input
   protected outputSchema: OUT;
@@ -61,11 +61,11 @@ export class Endpoint<IN extends ObjectSchema, OUT extends ObjectSchema, mIN, OP
     this.resultHandler = resultHandler;
   }
 
-  public getInputSchema(): ObjectSchema {
+  public getInputSchema(): IOSchema {
     return this.inputSchema;
   }
 
-  public getOutputSchema(): ObjectSchema {
+  public getOutputSchema(): IOSchema {
     return this.outputSchema;
   }
 
diff --git a/src/endpoints-factory.ts b/src/endpoints-factory.ts
index a8990263..6879616b 100644
--- a/src/endpoints-factory.ts
+++ b/src/endpoints-factory.ts
@@ -1,6 +1,6 @@
 import {z} from 'zod';
 import {Endpoint, Handler, Method} from './endpoint';
-import {FlatObject, Merge, ObjectSchema} from './helpers';
+import {FlatObject, IOSchema, Merge} from './helpers';
 import {MiddlewareDefinition} from './middleware';
 import {ResultHandler} from './result-handler';
 
@@ -24,14 +24,14 @@ export class EndpointsFactory<mIN, mOUT> {
     );
   }
 
-  public addMiddleware<IN extends ObjectSchema, OUT extends FlatObject>(definition: MiddlewareDefinition<IN, mOUT, OUT>) {
+  public addMiddleware<IN extends IOSchema, OUT extends FlatObject>(definition: MiddlewareDefinition<IN, mOUT, OUT>) {
     return new EndpointsFactory<Merge<IN, mIN>, mOUT & OUT>(
       this.middlewares.concat(definition),
       this.resultHandler
     );
   }
 
-  public build<IN extends ObjectSchema, OUT extends ObjectSchema>({methods, input, output, handler}: {
+  public build<IN extends IOSchema, OUT extends IOSchema>({methods, input, output, handler}: {
     methods: Method[],
     input: IN,
     output: OUT,
diff --git a/src/helpers.ts b/src/helpers.ts
index 79fb7b7b..4183f251 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -4,26 +4,56 @@ import {LoggerConfig, loggerLevels} from './config-type';
 import {MiddlewareDefinition} from './middleware';
 
 export type FlatObject = Record<string, any>;
-export type ObjectSchema = z.AnyZodObject;
+type EmptyFlatObject = {[K in never]: never};
 
-export type Merge<A extends ObjectSchema, B extends ObjectSchema | any> = z.ZodObject<
-  // eslint-disable-next-line @typescript-eslint/ban-types
-  A['shape'] & (B extends ObjectSchema ? B['shape'] : {}),
-  A['_unknownKeys'],
-  A['_catchall']
+type ObjectSchema = z.AnyZodObject;
+type Extractable = 'shape' | '_unknownKeys' | '_catchall';
+type UnionSchema = z.ZodUnion<[ObjectSchema, ...ObjectSchema[]]>;
+type IntersectionSchema = z.ZodIntersection<ObjectSchema, ObjectSchema>;
+
+export type IOSchema = ObjectSchema | UnionSchema | IntersectionSchema;
+
+type ArrayElement<T extends readonly unknown[]> = T extends readonly (infer K)[] ? K : never;
+
+type UnionResult<T extends ObjectSchema[], F extends Extractable> = Partial<ArrayElement<T>[F]>;
+type IntersectionResult<T extends IntersectionSchema, F extends Extractable> =
+  T['_def']['left'][F] & T['_def']['right'][F];
+
+type IOExtract<T extends IOSchema | any, F extends Extractable> =
+  T extends ObjectSchema ? T[F] :
+  T extends UnionSchema ? UnionResult<T['options'], F> :
+  T extends IntersectionSchema ? IntersectionResult<T, F> : EmptyFlatObject;
+
+export type Merge<A extends IOSchema, B extends IOSchema | any> = z.ZodObject<
+  IOExtract<A, 'shape'> & IOExtract<B, 'shape'>,
+  IOExtract<A, '_unknownKeys'>,
+  IOExtract<A, '_catchall'>
 >;
 
-export function combineEndpointAndMiddlewareInputSchemas<IN extends ObjectSchema, mIN>(
+export function extractObjectSchema(subject: IOSchema): ObjectSchema {
+  if (subject instanceof z.ZodUnion) {
+    return subject.options.reduce((acc, option) =>
+      acc.partial().merge(option.partial()));
+  }
+  if (subject instanceof z.ZodIntersection) {
+    return subject._def.left.merge(subject._def.right);
+  }
+  return subject;
+}
+
+export function combineEndpointAndMiddlewareInputSchemas<IN extends IOSchema, mIN>(
   input: IN,
-  middlewares: MiddlewareDefinition<any, any, any>[]
+  middlewares: MiddlewareDefinition<IOSchema, any, any>[]
 ): Merge<IN, mIN> {
   if (middlewares.length === 0) {
-    return input as any as Merge<IN, mIN>;
+    return extractObjectSchema(input) as Merge<IN, mIN>;
   }
-  return middlewares
+  const mSchema = middlewares
     .map((middleware) => middleware.input)
-    .reduce((carry: ObjectSchema, schema) => carry.merge(schema))
-    .merge(input) as Merge<IN, mIN>;
+    .reduce((carry, schema) =>
+      extractObjectSchema(carry).merge(extractObjectSchema(schema))
+    );
+  return extractObjectSchema(mSchema).merge(extractObjectSchema(input)) as Merge<IN, mIN>;
 }
 
 export function getInitialInput(request: Request): any {
diff --git a/src/index.ts b/src/index.ts
index 97b2522d..80a4951d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,7 @@
 export { ConfigType, LoggerConfig } from './config-type';
 export { AbstractEndpoint, Method, EndpointInput, EndpointOutput } from './endpoint';
 export { EndpointsFactory } from './endpoints-factory';
-export { ObjectSchema, FlatObject } from './helpers';
+export { IOSchema, FlatObject } from './helpers';
 export { createLogger } from './logger';
 export { createMiddleware } from './middleware';
 export { ResultHandler } from './result-handler';
diff --git a/src/middleware.ts b/src/middleware.ts
index d69923bb..c3cff599 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,7 +1,7 @@
 import {Request, Response} from 'express';
 import {Logger} from 'winston';
 import {z} from 'zod';
-import {FlatObject, ObjectSchema} from './helpers';
+import {FlatObject, IOSchema} from './helpers';
 
 interface MiddlewareParams<IN, OPT> {
   input: IN;
@@ -13,11 +13,11 @@ interface MiddlewareParams<IN, OPT> {
 
 type Middleware<IN, OPT, OUT> = (params: MiddlewareParams<IN, OPT>) => Promise<OUT>;
 
-export interface MiddlewareDefinition<IN extends ObjectSchema, OPT, OUT extends FlatObject> {
+export interface MiddlewareDefinition<IN extends IOSchema, OPT, OUT extends FlatObject> {
   input: IN;
   middleware: Middleware<z.output<IN>, OPT, OUT>;
 }
 
-export const createMiddleware = <IN extends ObjectSchema, OPT, OUT extends FlatObject>(
+export const createMiddleware = <IN extends IOSchema, OPT, OUT extends FlatObject>(
   definition: MiddlewareDefinition<IN, OPT, OUT>
 ) => definition;
diff --git a/src/open-api.ts b/src/open-api.ts
index 9c60e479..90fbe955 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -5,6 +5,7 @@ import {
 } from 'openapi3-ts';
 import {ReferenceObject} from 'openapi3-ts/src/model/OpenApi';
 import {z} from 'zod';
+import {extractObjectSchema} from './helpers';
 import {Routing, routingCycle} from './routing';
 import {lookup} from 'mime';
 
@@ -188,14 +189,15 @@ export class OpenAPI {
       };
       if (method === 'get') {
         operation.parameters = [];
-        Object.keys(endpoint.getInputSchema().shape).forEach((name) => {
+        const subject = extractObjectSchema(endpoint.getInputSchema()).shape;
+        Object.keys(subject).forEach((name) => {
           const parameterRef = this.createRef('parameter', 'parameters');
           this.builder.addParameter(parameterRef.name, {
             name,
             in: 'query',
-            required: !endpoint.getInputSchema().shape[name].isOptional(),
+            required: !subject[name].isOptional(),
             schema: {
-              ...describeSchema(endpoint.getInputSchema().shape[name], false),
+              ...describeSchema(subject[name], false),
               description: `${fullPath} ${method.toUpperCase()} parameter`
             },
           });


Test patch:
diff --git a/tests/unit/__snapshots__/helpers.spec.ts.snap b/tests/unit/__snapshots__/helpers.spec.ts.snap
index ef37972e..f9e8caea 100644
--- a/tests/unit/__snapshots__/helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/helpers.spec.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Helpers combineEndpointAndMiddlewareInputSchemas() Should merge input schemas 1`] = `
+exports[`Helpers combineEndpointAndMiddlewareInputSchemas() Should merge input object schemas 1`] = `
 Object {
   "four": ZodBoolean {
     "_def": Object {},
@@ -113,3 +113,918 @@ Object {
   },
 }
 `;
+
+exports[`Helpers combineEndpointAndMiddlewareInputSchemas() Should merge intersection object schemas 1`] = `
+Object {
+  "five": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+  "four": ZodBoolean {
+    "_def": Object {},
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "one": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+  "six": ZodNumber {
+    "_def": Object {
+      "isInteger": false,
+      "maximum": null,
+      "minimum": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "int": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "negative": [Function],
+    "nonnegative": [Function],
+    "nonpositive": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "positive": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "three": ZodNull {
+    "_def": Object {},
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "two": ZodNumber {
+    "_def": Object {
+      "isInteger": false,
+      "maximum": null,
+      "minimum": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "int": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "negative": [Function],
+    "nonnegative": [Function],
+    "nonpositive": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "positive": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+}
+`;
+
+exports[`Helpers combineEndpointAndMiddlewareInputSchemas() Should merge mixed object schemas 1`] = `
+Object {
+  "five": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+  "four": ZodOptional {
+    "_def": Object {
+      "innerType": ZodBoolean {
+        "_def": Object {},
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "one": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+  "three": ZodOptional {
+    "_def": Object {
+      "innerType": ZodNull {
+        "_def": Object {},
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "two": ZodNumber {
+    "_def": Object {
+      "isInteger": false,
+      "maximum": null,
+      "minimum": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "int": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "negative": [Function],
+    "nonnegative": [Function],
+    "nonpositive": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "positive": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+}
+`;
+
+exports[`Helpers combineEndpointAndMiddlewareInputSchemas() Should merge union object schemas 1`] = `
+Object {
+  "five": ZodOptional {
+    "_def": Object {
+      "innerType": ZodString {
+        "_def": Object {
+          "isEmail": false,
+          "isURL": false,
+          "isUUID": false,
+          "maxLength": null,
+          "minLength": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "_regex": [Function],
+        "array": [Function],
+        "default": [Function],
+        "email": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "nonempty": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "regex": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+        "url": [Function],
+        "uuid": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "four": ZodOptional {
+    "_def": Object {
+      "innerType": ZodBoolean {
+        "_def": Object {},
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "one": ZodOptional {
+    "_def": Object {
+      "innerType": ZodString {
+        "_def": Object {
+          "isEmail": false,
+          "isURL": false,
+          "isUUID": false,
+          "maxLength": null,
+          "minLength": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "_regex": [Function],
+        "array": [Function],
+        "default": [Function],
+        "email": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "nonempty": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "regex": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+        "url": [Function],
+        "uuid": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "six": ZodOptional {
+    "_def": Object {
+      "innerType": ZodNumber {
+        "_def": Object {
+          "isInteger": false,
+          "maximum": null,
+          "minimum": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "int": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "negative": [Function],
+        "nonnegative": [Function],
+        "nonpositive": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "positive": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "three": ZodOptional {
+    "_def": Object {
+      "innerType": ZodNull {
+        "_def": Object {},
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "two": ZodOptional {
+    "_def": Object {
+      "innerType": ZodNumber {
+        "_def": Object {
+          "isInteger": false,
+          "maximum": null,
+          "minimum": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "int": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "negative": [Function],
+        "nonnegative": [Function],
+        "nonpositive": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "positive": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+}
+`;
+
+exports[`Helpers extractObjectSchema() should pass the object schema through 1`] = `
+Object {
+  "one": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+}
+`;
+
+exports[`Helpers extractObjectSchema() should return object schema for the intersection of object schemas 1`] = `
+Object {
+  "one": ZodString {
+    "_def": Object {
+      "isEmail": false,
+      "isURL": false,
+      "isUUID": false,
+      "maxLength": null,
+      "minLength": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "_regex": [Function],
+    "array": [Function],
+    "default": [Function],
+    "email": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "nonempty": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "regex": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+    "url": [Function],
+    "uuid": [Function],
+  },
+  "two": ZodNumber {
+    "_def": Object {
+      "isInteger": false,
+      "maximum": null,
+      "minimum": null,
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "int": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "max": [Function],
+    "min": [Function],
+    "negative": [Function],
+    "nonnegative": [Function],
+    "nonpositive": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "positive": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+}
+`;
+
+exports[`Helpers extractObjectSchema() should return object schema for the union of object schemas 1`] = `
+Object {
+  "one": ZodOptional {
+    "_def": Object {
+      "innerType": ZodString {
+        "_def": Object {
+          "isEmail": false,
+          "isURL": false,
+          "isUUID": false,
+          "maxLength": null,
+          "minLength": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "_regex": [Function],
+        "array": [Function],
+        "default": [Function],
+        "email": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "nonempty": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "regex": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+        "url": [Function],
+        "uuid": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+  "two": ZodOptional {
+    "_def": Object {
+      "innerType": ZodNumber {
+        "_def": Object {
+          "isInteger": false,
+          "maximum": null,
+          "minimum": null,
+        },
+        "_parseInternalOptionalParams": [Function],
+        "_parseWithInvalidFallback": [Function],
+        "array": [Function],
+        "default": [Function],
+        "int": [Function],
+        "isNullable": [Function],
+        "isOptional": [Function],
+        "max": [Function],
+        "min": [Function],
+        "negative": [Function],
+        "nonnegative": [Function],
+        "nonpositive": [Function],
+        "nullable": [Function],
+        "nullish": [Function],
+        "optional": [Function],
+        "parse": [Function],
+        "parseAsync": [Function],
+        "positive": [Function],
+        "refine": [Function],
+        "refinement": [Function],
+        "safeParse": [Function],
+        "safeParseAsync": [Function],
+        "spa": [Function],
+        "superRefine": [Function],
+        "transform": [Function],
+      },
+    },
+    "_parseInternalOptionalParams": [Function],
+    "_parseWithInvalidFallback": [Function],
+    "array": [Function],
+    "default": [Function],
+    "isNullable": [Function],
+    "isOptional": [Function],
+    "nullable": [Function],
+    "nullish": [Function],
+    "optional": [Function],
+    "parse": [Function],
+    "parseAsync": [Function],
+    "refine": [Function],
+    "refinement": [Function],
+    "safeParse": [Function],
+    "safeParseAsync": [Function],
+    "spa": [Function],
+    "superRefine": [Function],
+    "transform": [Function],
+  },
+}
+`;
diff --git a/tests/unit/helpers.spec.ts b/tests/unit/helpers.spec.ts
index b4231f0d..28f5e1dc 100644
--- a/tests/unit/helpers.spec.ts
+++ b/tests/unit/helpers.spec.ts
@@ -1,10 +1,15 @@
-import {combineEndpointAndMiddlewareInputSchemas, getInitialInput, isLoggerConfig} from '../../src/helpers';
+import {
+  combineEndpointAndMiddlewareInputSchemas,
+  extractObjectSchema,
+  getInitialInput,
+  isLoggerConfig
+} from '../../src/helpers';
 import {createMiddleware, z} from '../../src';
 import {Request} from 'express';
 
 describe('Helpers', () => {
   describe('combineEndpointAndMiddlewareInputSchemas()', () => {
-    test('Should merge input schemas', () => {
+    test('Should merge input object schemas', () => {
       const middlewares = [
         createMiddleware({
           input: z.object({
@@ -32,6 +37,91 @@ describe('Helpers', () => {
       expect(result).toBeInstanceOf(z.ZodObject);
       expect(result.shape).toMatchSnapshot();
     });
+
+    test('Should merge union object schemas', () => {
+      const middlewares = [
+        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 = combineEndpointAndMiddlewareInputSchemas(endpointInput, middlewares);
+      expect(result).toBeInstanceOf(z.ZodObject);
+      expect(result.shape).toMatchSnapshot();
+    });
+
+    test('Should merge intersection object schemas', () => {
+      const middlewares = [
+        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 = combineEndpointAndMiddlewareInputSchemas(endpointInput, middlewares);
+      expect(result).toBeInstanceOf(z.ZodObject);
+      expect(result.shape).toMatchSnapshot();
+    });
+
+    test('Should merge mixed object schemas', () => {
+      const middlewares = [
+        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 = combineEndpointAndMiddlewareInputSchemas(endpointInput, middlewares);
+      expect(result).toBeInstanceOf(z.ZodObject);
+      expect(result.shape).toMatchSnapshot();
+    });
   });
   
   describe('getInitialInput()', () => {
@@ -97,4 +187,34 @@ describe('Helpers', () => {
       expect(isLoggerConfig('something')).toBeFalsy();
     });
   });
+
+  describe('extractObjectSchema()', () => {
+    test('should pass the object schema through', () => {
+      const subject = extractObjectSchema(z.object({
+        one: z.string()
+      }));
+      expect(subject).toBeInstanceOf(z.ZodObject);
+      expect(subject.shape).toMatchSnapshot();
+    });
+
+    test('should return object schema for the union of object schemas', () => {
+      const subject = extractObjectSchema(z.object({
+        one: z.string()
+      }).or(z.object({
+        two: z.number()
+      })));
+      expect(subject).toBeInstanceOf(z.ZodObject);
+      expect(subject.shape).toMatchSnapshot();
+    });
+
+    test('should return object schema for the intersection of object schemas', () => {
+      const subject = extractObjectSchema(z.object({
+        one: z.string()
+      }).and(z.object({
+        two: z.number()
+      })));
+      expect(subject).toBeInstanceOf(z.ZodObject);
+      expect(subject.shape).toMatchSnapshot();
+    });
+  });
 });