Repository: RobinTail/express-zod-api
Pull request #340 (2022-03-05 13:36:40)

v6: Replacing Merge with ZodIntersection
### Improvements

- better generated documentation of `ZodUnion`s on the top level
- ability to use `ZodDiscriminatedUnion` on the top level

### Breaking changes

- Type narrowing: You cannot use the `.strict()`, `.passthrough()` and its deprecated alias `.nonstrict()` methods in middleware object schemas. 
- Only `.strip()` is allowed in middlewares, which is actually default, so you should not use any of them at all.

RobinTail: Ready for experiment ✅ 
RobinTail: Released as `5.9.0-beta1` via `beta` channel 🚀 
RobinTail: Operational ✅ 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07c808fc..30bcb958 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,64 @@
 # Changelog
 
+## Version 6
+
+### v6.0.0
+
+- Technically this version contains all the same changes and improvements as 5.9.0-beta1.
+- The new implementation of the `EndpointsFactory`, however, has more restrictive middleware input schema requirements.
+- To avoid possible backward incompatibility issues, I have decided to publish these changes as a major release.
+- The following changes are required to migrate to this version:
+  - You cannot use the `.strict()`, `.passthrough()` and its deprecated alias `.nonstrict()` methods in middlewares.
+  - Only `.strip()` is allowed in middlewares, which is actually default, so you should not use any of them at all.
+
+```typescript
+// how to migrate
+export const myMiddleware = createMiddleware({
+  input: z
+    .object({
+      key: z.string().nonempty(),
+    })
+    .passthrough(), // <— remove this if you have it in your code
+  middleware: async () => ({...}),
+});
+
+```
+
 ## Version 5
 
+### v5.9.0-beta1
+
+- In this build, improvements have been made to the `EndpointsFactory`, in terms of combining the input schemas of
+  middlewares and the endpoint itself. A custom type has been replaced with usage of `ZodIntersection` schema with
+  respect to the originals.
+- The generated documentation has improved in this regard:
+  - Previously, fields from an object union were documented in a simplified way as optional.
+  - Instead, it is now documented using `oneOf` OpenAPI notation.
+- In addition, you can now also use the new `z.discriminatedUnion()` as the input schema on the top level.
+
+```typescript
+// example
+const endpoint = defaultEndpointsFactory.build({
+  method: "post",
+  input: z.discriminatedUnion("type", [
+    z.object({
+      type: z.literal("text"),
+      str: z.string()
+    }),
+    z.object({
+      type: z.literal("numeric"),
+      num: z.number()
+    }),
+  ]),
+  output: z.object({...}),
+  handler: async ({ input }) => {
+    // the type of the input:
+    // | { type: "text", str: string }
+    // | { type: "numeric", num: number }
+  }
+});
+```
+
 ### v5.8.0
 
 - `zod` version is 3.13.4.
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index 97556622..b54a8892 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -1,7 +1,7 @@
 openapi: 3.0.0
 info:
   title: Example API
-  version: 5.8.0
+  version: 5.9.0-beta1
 paths:
   /v1/user/retrieve:
     get:
@@ -155,25 +155,34 @@ paths:
           application/json:
             schema:
               description: POST /v1/user/:id request body
-              type: object
-              properties:
-                key:
-                  type: string
-                  minLength: 1
-                name:
-                  type: string
-                  minLength: 1
-                birthday:
-                  type: string
-                  format: date-time
-                  pattern: ^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?)?Z?$
-                  description: YYYY-MM-DDTHH:mm:ss.sssZ
-                  externalDocs:
-                    url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
-              required:
-                - key
-                - name
-                - birthday
+              allOf:
+                - example:
+                    key: 1234-5678-90
+                  type: object
+                  properties:
+                    key:
+                      type: string
+                      minLength: 1
+                  required:
+                    - key
+                - example:
+                    name: John Doe
+                    birthday: 1963-04-21
+                  type: object
+                  properties:
+                    name:
+                      type: string
+                      minLength: 1
+                    birthday:
+                      type: string
+                      format: date-time
+                      pattern: ^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?)?Z?$
+                      description: YYYY-MM-DDTHH:mm:ss.sssZ
+                      externalDocs:
+                        url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+                  required:
+                    - name
+                    - birthday
             examples:
               example1:
                 value:
diff --git a/package.json b/package.json
index 8c2431df..acd6fe9b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "express-zod-api",
-  "version": "5.8.0",
+  "version": "5.9.0-beta1",
   "description": "A Typescript library to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.",
   "license": "MIT",
   "scripts": {
diff --git a/src/common-helpers.ts b/src/common-helpers.ts
index 08d6f61a..a0ecf8a5 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -9,56 +9,21 @@ import {
 } from "./config-type";
 import { copyMeta, getMeta } from "./metadata";
 import { Method } from "./method";
-import { MiddlewareDefinition } from "./middleware";
+import { AnyMiddlewareDef } from "./middleware";
 import { mimeMultipart } from "./mime";
 import { ZodUpload } from "./upload-schema";
 
 export type FlatObject = Record<string, any>;
 
-type ObjectSchema = z.AnyZodObject;
-type Extractable =
-  | "shape"
-  | "_unknownKeys"
-  | "_catchall"
-  | "_output"
-  | "_input";
-type UnionSchema = z.ZodUnion<[ObjectSchema, ...ObjectSchema[]]>;
-type IntersectionSchema = z.ZodIntersection<ObjectSchema, ObjectSchema>;
-
-// @todo should support DiscriminatedUnion too
-export type IOSchema = ObjectSchema | UnionSchema | IntersectionSchema;
+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>>;
 
 export type ArrayElement<T extends readonly unknown[]> =
   T extends readonly (infer K)[] ? K : never;
 
-type UnionResult<
-  T extends ObjectSchema[],
-  F extends Extractable
-> = 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>
-  : unknown;
-
-export type Merge<A extends IOSchema, B extends IOSchema | any> = z.ZodObject<
-  IOExtract<A, "shape"> & IOExtract<B, "shape">,
-  IOExtract<A, "_unknownKeys">,
-  IOExtract<A, "_catchall">,
-  IOExtract<A, "_output"> & IOExtract<B, "_output">,
-  IOExtract<A, "_input"> & IOExtract<B, "_input">
->;
-
 export type OutputMarker = IOSchema & { _typeGuard: "OutputMarker" };
 export const markOutput = (output: IOSchema) => output as OutputMarker;
 
@@ -72,46 +37,36 @@ export type ReplaceMarkerInShape<
   [K in keyof S]: S[K] extends OutputMarker ? OUT : S[K];
 };
 
-export function extractObjectSchema(subject: IOSchema): ObjectSchema {
-  if (subject instanceof z.ZodObject) {
-    return subject;
-  }
-  let objectSchema: ObjectSchema;
-  if (subject instanceof z.ZodUnion) {
-    objectSchema = subject.options.reduce((acc, option) =>
-      acc.partial().merge(option.partial())
-    );
-  } else {
-    // intersection schema
-    objectSchema = subject._def.left.merge(subject._def.right);
-  }
-  return copyMeta(subject, objectSchema);
-}
-
-export function combineEndpointAndMiddlewareInputSchemas<
-  IN extends IOSchema,
-  MwIN
+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
 >(
-  input: IN,
-  middlewares: MiddlewareDefinition<IOSchema, any, any>[]
-): Merge<IN, MwIN> {
-  if (middlewares.length === 0) {
-    return extractObjectSchema(input) as Merge<IN, MwIN>;
-  }
-  const mSchema = middlewares
-    .map((middleware) => middleware.input)
-    .reduce((carry, schema) =>
-      extractObjectSchema(carry).merge(extractObjectSchema(schema))
-    );
-  const result = extractObjectSchema(mSchema).merge(
-    extractObjectSchema(input)
-  ) as Merge<IN, MwIN>;
+  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") || "";
@@ -275,3 +230,6 @@ 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/depends-on-method.ts b/src/depends-on-method.ts
index 81c89362..cca09ebd 100644
--- a/src/depends-on-method.ts
+++ b/src/depends-on-method.ts
@@ -6,8 +6,8 @@ export class DependsOnMethod {
   constructor(
     public readonly methods: {
       [K in Method]?:
-        | Endpoint<any, any, any, any, K, any, any>
-        | Endpoint<any, any, any, any, Method, any, any>;
+        | Endpoint<any, any, any, K, any, any>
+        | Endpoint<any, any, any, Method, any, any>;
     }
   ) {
     (Object.keys(methods) as (keyof typeof methods)[]).forEach((key) => {
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 6d49c449..f0f9700a 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -5,15 +5,14 @@ import { ApiResponse } from "./api-response";
 import { CommonConfig } from "./config-type";
 import { ResultHandlerError } from "./errors";
 import {
-  combineEndpointAndMiddlewareInputSchemas,
+  FlatObject,
   getInitialInput,
   IOSchema,
-  Merge,
   OutputMarker,
   ReplaceMarkerInShape,
 } from "./common-helpers";
 import { Method, MethodsDefinition } from "./method";
-import { MiddlewareDefinition } from "./middleware";
+import { AnyMiddlewareDef } from "./middleware";
 import { lastResortHandler, ResultHandlerDefinition } from "./result-handler";
 
 export type Handler<IN, OUT, OPT> = (params: {
@@ -43,13 +42,12 @@ export abstract class AbstractEndpoint {
 export type EndpointInput<T> = T extends Endpoint<
   infer IN,
   any,
-  infer mIN,
   any,
   any,
   any,
   any
 >
-  ? z.input<Merge<IN, mIN>>
+  ? z.input<IN>
   : never;
 
 export type EndpointOutput<T> = T extends Endpoint<
@@ -58,7 +56,6 @@ export type EndpointOutput<T> = T extends Endpoint<
   any,
   any,
   any,
-  any,
   any
 >
   ? z.output<OUT>
@@ -84,42 +81,35 @@ export type EndpointResponse<E extends AbstractEndpoint> =
 type EndpointProps<
   IN extends IOSchema,
   OUT extends IOSchema,
-  MwIN,
-  OPT,
+  OPT extends FlatObject,
   M extends Method,
   POS extends ApiResponse,
   NEG extends ApiResponse
 > = {
-  middlewares: MiddlewareDefinition<any, any, any>[];
+  middlewares: AnyMiddlewareDef[];
   inputSchema: IN;
   mimeTypes: string[];
   outputSchema: OUT;
-  handler: Handler<z.output<Merge<IN, MwIN>>, z.input<OUT>, OPT>;
+  handler: Handler<z.output<IN>, z.input<OUT>, OPT>;
   resultHandler: ResultHandlerDefinition<POS, NEG>;
   description?: string;
 } & MethodsDefinition<M>;
 
-/** mIN, OPT - from Middlewares */
 export class Endpoint<
   IN extends IOSchema,
   OUT extends IOSchema,
-  MwIN,
-  OPT,
+  OPT extends FlatObject,
   M extends Method,
   POS extends ApiResponse,
   NEG extends ApiResponse
 > extends AbstractEndpoint {
   protected readonly description?: string;
   protected readonly methods: M[] = [];
-  protected readonly middlewares: MiddlewareDefinition<any, any, any>[] = [];
-  protected readonly inputSchema: Merge<IN, MwIN>; // combined with middlewares input
+  protected readonly middlewares: AnyMiddlewareDef[] = [];
+  protected readonly inputSchema: IN;
   protected readonly mimeTypes: string[];
   protected readonly outputSchema: OUT;
-  protected readonly handler: Handler<
-    z.output<Merge<IN, MwIN>>,
-    z.input<OUT>,
-    OPT
-  >;
+  protected readonly handler: Handler<z.output<IN>, z.input<OUT>, OPT>;
   protected readonly resultHandler: ResultHandlerDefinition<POS, NEG>;
 
   constructor({
@@ -131,13 +121,10 @@ export class Endpoint<
     description,
     mimeTypes,
     ...rest
-  }: EndpointProps<IN, OUT, MwIN, OPT, M, POS, NEG>) {
+  }: EndpointProps<IN, OUT, OPT, M, POS, NEG>) {
     super();
     this.middlewares = middlewares;
-    this.inputSchema = combineEndpointAndMiddlewareInputSchemas<IN, MwIN>(
-      inputSchema,
-      middlewares
-    );
+    this.inputSchema = inputSchema;
     this.mimeTypes = mimeTypes;
     this.outputSchema = outputSchema;
     this.handler = handler;
@@ -158,7 +145,7 @@ export class Endpoint<
     return this.methods;
   }
 
-  public override getInputSchema(): Merge<IN, MwIN> {
+  public override getInputSchema(): IN {
     return this.inputSchema;
   }
 
@@ -264,7 +251,8 @@ export class Endpoint<
     logger: Logger;
   }) {
     return this.handler({
-      input: await this.inputSchema.parseAsync(input), // final input types transformations for handler,
+      // final input types transformations for handler
+      input: (await this.inputSchema.parseAsync(input)) as z.output<IN>,
       options,
       logger,
     });
diff --git a/src/endpoints-factory.ts b/src/endpoints-factory.ts
index 97f9ed4b..ebe19358 100644
--- a/src/endpoints-factory.ts
+++ b/src/endpoints-factory.ts
@@ -1,10 +1,17 @@
 import { Request, Response } from "express";
 import { z } from "zod";
 import { ApiResponse } from "./api-response";
+import {
+  FlatObject,
+  getFinalEndpointInputSchema,
+  hasUpload,
+  IOSchema,
+  ProbableIntersection,
+} from "./common-helpers";
 import { Endpoint, Handler } from "./endpoint";
-import { FlatObject, IOSchema, hasUpload, Merge } from "./common-helpers";
 import { Method, MethodsDefinition } from "./method";
 import {
+  AnyMiddlewareDef,
   createMiddleware,
   ExpressMiddleware,
   ExpressMiddlewareFeatures,
@@ -19,49 +26,50 @@ import {
 type BuildProps<
   IN extends IOSchema,
   OUT extends IOSchema,
-  MwIN,
-  MwOUT,
+  MIN extends IOSchema<"strip"> | null,
+  OPT extends FlatObject,
   M extends Method
 > = {
   input: IN;
   output: OUT;
-  handler: Handler<z.output<Merge<IN, MwIN>>, z.input<OUT>, MwOUT>;
+  handler: Handler<z.output<ProbableIntersection<MIN, IN>>, z.input<OUT>, OPT>;
   description?: string;
 } & MethodsDefinition<M>;
 
 export class EndpointsFactory<
-  MwIN,
-  MwOUT,
   POS extends ApiResponse,
-  NEG extends ApiResponse
+  NEG extends ApiResponse,
+  IN extends IOSchema<"strip"> | null = null,
+  OUT extends FlatObject = {}
 > {
-  protected middlewares: MiddlewareDefinition<any, any, any>[] = [];
+  protected middlewares: AnyMiddlewareDef[] = [];
 
-  constructor(protected resultHandler: ResultHandlerDefinition<POS, NEG>) {
-    this.resultHandler = resultHandler;
-  }
+  constructor(protected resultHandler: ResultHandlerDefinition<POS, NEG>) {}
 
   static #create<
-    CrMwIN,
-    CrMwOUT,
-    CrPOS extends ApiResponse,
-    CrNEG extends ApiResponse
+    CPOS extends ApiResponse,
+    CNEG extends ApiResponse,
+    CIN extends IOSchema<"strip"> | null,
+    COUT extends FlatObject
   >(
-    middlewares: MiddlewareDefinition<any, any, any>[],
-    resultHandler: ResultHandlerDefinition<CrPOS, CrNEG>
+    middlewares: AnyMiddlewareDef[],
+    resultHandler: ResultHandlerDefinition<CPOS, CNEG>
   ) {
-    const factory = new EndpointsFactory<CrMwIN, CrMwOUT, CrPOS, CrNEG>(
-      resultHandler
-    );
+    const factory = new EndpointsFactory<CPOS, CNEG, CIN, COUT>(resultHandler);
     factory.middlewares = middlewares;
     return factory;
   }
 
-  public addMiddleware<IN extends IOSchema, OUT extends FlatObject>(
-    definition: MiddlewareDefinition<IN, MwOUT, OUT>
+  public addMiddleware<AIN extends IOSchema<"strip">, AOUT extends FlatObject>(
+    definition: MiddlewareDefinition<AIN, OUT, AOUT>
   ) {
-    return EndpointsFactory.#create<Merge<IN, MwIN>, MwOUT & OUT, POS, NEG>(
-      this.middlewares.concat(definition),
+    return EndpointsFactory.#create<
+      POS,
+      NEG,
+      ProbableIntersection<IN, AIN>,
+      OUT & AOUT
+    >(
+      this.middlewares.concat(definition as unknown as AnyMiddlewareDef),
       this.resultHandler
     );
   }
@@ -71,17 +79,17 @@ export class EndpointsFactory<
   public addExpressMiddleware<
     R extends Request,
     S extends Response,
-    OUT extends FlatObject = {}
+    AOUT extends FlatObject = {}
   >(
     middleware: ExpressMiddleware<R, S>,
-    features?: ExpressMiddlewareFeatures<R, S, OUT>
+    features?: ExpressMiddlewareFeatures<R, S, AOUT>
   ) {
     const transformer = features?.transformer || ((err: Error) => err);
-    const provider = features?.provider || (() => ({} as OUT));
+    const provider = features?.provider || (() => ({} as AOUT));
     const definition = createMiddleware({
       input: z.object({}),
       middleware: async ({ request, response }) =>
-        new Promise<OUT>((resolve, reject) => {
+        new Promise<AOUT>((resolve, reject) => {
           const next = (err?: any) => {
             if (err && err instanceof Error) {
               return reject(transformer(err));
@@ -91,38 +99,46 @@ export class EndpointsFactory<
           middleware(request as R, response as S, next);
         }),
     });
-    return EndpointsFactory.#create<MwIN, MwOUT & OUT, POS, NEG>(
-      this.middlewares.concat(definition),
+    return EndpointsFactory.#create<POS, NEG, IN, OUT & AOUT>(
+      this.middlewares.concat(definition as AnyMiddlewareDef),
       this.resultHandler
     );
   }
 
-  public addOptions<OUT extends FlatObject>(options: OUT) {
-    return EndpointsFactory.#create<MwIN, MwOUT & OUT, POS, NEG>(
+  public addOptions<AOUT extends FlatObject>(options: AOUT) {
+    return EndpointsFactory.#create<POS, NEG, IN, OUT & AOUT>(
       this.middlewares.concat(
         createMiddleware({
           input: z.object({}),
           middleware: async () => options,
-        })
+        }) as AnyMiddlewareDef
       ),
       this.resultHandler
     );
   }
 
-  public build<IN extends IOSchema, OUT extends IOSchema, M extends Method>({
+  public build<BIN extends IOSchema, BOUT extends IOSchema, M extends Method>({
     input,
-    output,
     handler,
     description,
+    output: outputSchema,
     ...rest
-  }: BuildProps<IN, OUT, MwIN, MwOUT, M>) {
-    return new Endpoint<IN, OUT, MwIN, MwOUT, M, POS, NEG>({
+  }: BuildProps<BIN, BOUT, IN, OUT, M>): Endpoint<
+    ProbableIntersection<IN, BIN>,
+    BOUT,
+    OUT,
+    M,
+    POS,
+    NEG
+  > {
+    const { middlewares, resultHandler } = this;
+    return new Endpoint({
       handler,
       description,
-      middlewares: this.middlewares,
-      inputSchema: input,
-      outputSchema: output,
-      resultHandler: this.resultHandler,
+      middlewares,
+      outputSchema,
+      resultHandler,
+      inputSchema: getFinalEndpointInputSchema<IN, BIN>(middlewares, input),
       mimeTypes: hasUpload(input) ? [mimeMultipart] : [mimeJson],
       ...rest,
     });
diff --git a/src/middleware.ts b/src/middleware.ts
index 2c7e0929..654a2865 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -17,7 +17,7 @@ type Middleware<IN, OPT, OUT> = (
 ) => Promise<OUT>;
 
 export interface MiddlewareDefinition<
-  IN extends IOSchema,
+  IN extends IOSchema<"strip">,
   OPT,
   OUT extends FlatObject
 > {
@@ -25,8 +25,14 @@ export interface MiddlewareDefinition<
   middleware: Middleware<z.output<IN>, OPT, OUT>;
 }
 
+export type AnyMiddlewareDef = MiddlewareDefinition<
+  IOSchema<"strip">,
+  any,
+  any
+>;
+
 export const createMiddleware = <
-  IN extends IOSchema,
+  IN extends IOSchema<"strip">,
   OPT,
   OUT extends FlatObject
 >(
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 5a0c029c..1222714a 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -10,10 +10,10 @@ import {
   RequestBodyObject,
   ResponseObject,
 } from "openapi3-ts/src/model/OpenApi";
+import { omit } from "ramda";
 import { z } from "zod";
 import {
   ArrayElement,
-  extractObjectSchema,
   getExamples,
   getRoutePathParams,
   IOSchema,
@@ -25,9 +25,9 @@ import { ZodDateOut, ZodDateOutDef } from "./date-out-schema";
 import { AbstractEndpoint } from "./endpoint";
 import { OpenAPIError } from "./errors";
 import { ZodFile, ZodFileDef } from "./file-schema";
+import { copyMeta } from "./metadata";
 import { Method } from "./method";
 import { ZodUpload, ZodUploadDef } from "./upload-schema";
-import { omit } from "ramda";
 
 type MediaExamples = Pick<MediaTypeObject, "examples">;
 
@@ -540,6 +540,27 @@ export const depictIOParamExamples = <T extends IOSchema>(
   };
 };
 
+export function extractObjectSchema(subject: IOSchema) {
+  if (subject instanceof z.ZodObject) {
+    return subject;
+  }
+  let objectSchema: z.AnyZodObject;
+  if (
+    subject instanceof z.ZodUnion ||
+    subject instanceof z.ZodDiscriminatedUnion
+  ) {
+    objectSchema = Array.from(subject.options.values())
+      .map((option) => extractObjectSchema(option))
+      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
+  } else {
+    // intersection schema
+    objectSchema = extractObjectSchema(subject._def.left).merge(
+      extractObjectSchema(subject._def.right)
+    );
+  }
+  return copyMeta(subject, objectSchema);
+}
+
 export const depictRequestParams = ({
   path,
   method,
@@ -632,6 +653,9 @@ export const excludeParamsFromDepiction = (
   const properties = depicted.properties
     ? omit(pathParams, depicted.properties)
     : undefined;
+  const example = depicted.example
+    ? omit(pathParams, depicted.example)
+    : undefined;
   const required = depicted.required
     ? depicted.required.filter((name) => !pathParams.includes(name))
     : undefined;
@@ -647,13 +671,14 @@ export const excludeParamsFromDepiction = (
     : undefined;
 
   return omit(
-    Object.entries({ properties, required, allOf, oneOf })
+    Object.entries({ properties, required, example, allOf, oneOf })
       .filter(([{}, value]) => value === undefined)
       .map(([key]) => key),
     {
       ...depicted,
       properties,
       required,
+      example,
       allOf,
       oneOf,
     }


Test patch:
diff --git a/tests/helpers.ts b/tests/helpers.ts
index e4f04a8e..e537414d 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -31,9 +31,12 @@ export const serializeSchemaForTest = (
           left: serializeSchemaForTest(schema._def.left),
           right: serializeSchemaForTest(schema._def.right),
         }
-      : schema instanceof z.ZodUnion
+      : schema instanceof z.ZodUnion ||
+        schema instanceof z.ZodDiscriminatedUnion
       ? {
-          options: schema._def.options.map(serializeSchemaForTest),
+          options: Array.from(schema.options.values()).map((option) =>
+            serializeSchemaForTest(option as z.ZodTypeAny)
+          ),
         }
       : schema instanceof z.ZodObject
       ? {
diff --git a/tests/unit/__snapshots__/common-helpers.spec.ts.snap b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
index 8d9dce4b..eb5d1a3f 100644
--- a/tests/unit/__snapshots__/common-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/common-helpers.spec.ts.snap
@@ -1,192 +1,278 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Common Helpers combineEndpointAndMiddlewareInputSchemas() Should merge input object schemas 1`] = `
+exports[`Common Helpers defaultInputSources should be declared in a certain way 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "four": Object {
-      "_type": "ZodBoolean",
-    },
-    "one": Object {
-      "_type": "ZodString",
-    },
-    "three": Object {
-      "_type": "ZodNull",
-    },
-    "two": Object {
-      "_type": "ZodNumber",
-    },
-  },
+  "delete": Array [
+    "body",
+    "query",
+    "params",
+  ],
+  "get": Array [
+    "query",
+    "params",
+  ],
+  "patch": Array [
+    "body",
+    "params",
+  ],
+  "post": Array [
+    "body",
+    "params",
+    "files",
+  ],
+  "put": Array [
+    "body",
+    "params",
+  ],
 }
 `;
 
-exports[`Common Helpers combineEndpointAndMiddlewareInputSchemas() Should merge intersection object schemas 1`] = `
+exports[`Common Helpers getFinalEndpointInputSchema() Should handle no middlewares 1`] = `
 Object {
   "_type": "ZodObject",
   "shape": Object {
-    "five": Object {
-      "_type": "ZodString",
-    },
     "four": Object {
       "_type": "ZodBoolean",
     },
-    "one": Object {
-      "_type": "ZodString",
-    },
-    "six": Object {
-      "_type": "ZodNumber",
-    },
-    "three": Object {
-      "_type": "ZodNull",
-    },
-    "two": Object {
-      "_type": "ZodNumber",
-    },
   },
 }
 `;
 
-exports[`Common Helpers combineEndpointAndMiddlewareInputSchemas() Should merge mixed object schemas 1`] = `
+exports[`Common Helpers getFinalEndpointInputSchema() Should merge input object schemas 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "five": Object {
-      "_type": "ZodString",
-    },
-    "four": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodBoolean",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodIntersection",
+      "left": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "one": Object {
+            "_type": "ZodString",
+          },
+        },
+      },
+      "right": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "two": Object {
+            "_type": "ZodNumber",
+          },
+        },
       },
     },
-    "one": Object {
-      "_type": "ZodString",
-    },
-    "three": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNull",
+    "right": Object {
+      "_type": "ZodObject",
+      "shape": Object {
+        "three": Object {
+          "_type": "ZodNull",
+        },
       },
     },
-    "two": Object {
-      "_type": "ZodNumber",
+  },
+  "right": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "four": Object {
+        "_type": "ZodBoolean",
+      },
     },
   },
 }
 `;
 
-exports[`Common Helpers combineEndpointAndMiddlewareInputSchemas() Should merge union object schemas 1`] = `
+exports[`Common Helpers getFinalEndpointInputSchema() Should merge intersection object schemas 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "five": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodString",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodIntersection",
+      "left": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "one": Object {
+            "_type": "ZodString",
+          },
+        },
       },
-    },
-    "four": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodBoolean",
+      "right": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "two": Object {
+            "_type": "ZodNumber",
+          },
+        },
       },
     },
-    "one": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodString",
+    "right": Object {
+      "_type": "ZodIntersection",
+      "left": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "three": Object {
+            "_type": "ZodNull",
+          },
+        },
       },
-    },
-    "six": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNumber",
+      "right": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "four": Object {
+            "_type": "ZodBoolean",
+          },
+        },
       },
     },
-    "three": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNull",
+  },
+  "right": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodObject",
+      "shape": Object {
+        "five": Object {
+          "_type": "ZodString",
+        },
       },
     },
-    "two": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNumber",
+    "right": Object {
+      "_type": "ZodObject",
+      "shape": Object {
+        "six": Object {
+          "_type": "ZodNumber",
+        },
       },
     },
   },
 }
 `;
 
-exports[`Common Helpers defaultInputSources should be declared in a certain way 1`] = `
+exports[`Common Helpers getFinalEndpointInputSchema() Should merge mixed object schemas 1`] = `
 Object {
-  "delete": Array [
-    "body",
-    "query",
-    "params",
-  ],
-  "get": Array [
-    "query",
-    "params",
-  ],
-  "patch": Array [
-    "body",
-    "params",
-  ],
-  "post": Array [
-    "body",
-    "params",
-    "files",
-  ],
-  "put": Array [
-    "body",
-    "params",
-  ],
-}
-`;
-
-exports[`Common Helpers extractObjectSchema() should pass the object schema through 1`] = `
-Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "one": Object {
-      "_type": "ZodString",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodIntersection",
+      "left": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "one": Object {
+            "_type": "ZodString",
+          },
+        },
+      },
+      "right": Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "two": Object {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    },
+    "right": Object {
+      "_type": "ZodUnion",
+      "options": Array [
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "three": Object {
+              "_type": "ZodNull",
+            },
+          },
+        },
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "four": Object {
+              "_type": "ZodBoolean",
+            },
+          },
+        },
+      ],
+    },
+  },
+  "right": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "five": Object {
+        "_type": "ZodString",
+      },
     },
   },
 }
 `;
 
-exports[`Common Helpers extractObjectSchema() should return object schema for the intersection of object schemas 1`] = `
+exports[`Common Helpers getFinalEndpointInputSchema() Should merge union object schemas 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "one": Object {
-      "_type": "ZodString",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodUnion",
+      "options": Array [
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "one": Object {
+              "_type": "ZodString",
+            },
+          },
+        },
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "two": Object {
+              "_type": "ZodNumber",
+            },
+          },
+        },
+      ],
     },
-    "two": Object {
-      "_type": "ZodNumber",
+    "right": Object {
+      "_type": "ZodUnion",
+      "options": Array [
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "three": Object {
+              "_type": "ZodNull",
+            },
+          },
+        },
+        Object {
+          "_type": "ZodObject",
+          "shape": Object {
+            "four": Object {
+              "_type": "ZodBoolean",
+            },
+          },
+        },
+      ],
     },
   },
-}
-`;
-
-exports[`Common Helpers extractObjectSchema() should return object schema for the union of object schemas 1`] = `
-Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "one": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodString",
+  "right": Object {
+    "_type": "ZodUnion",
+    "options": Array [
+      Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "five": Object {
+            "_type": "ZodString",
+          },
+        },
       },
-    },
-    "two": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNumber",
+      Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "six": Object {
+            "_type": "ZodNumber",
+          },
+        },
       },
-    },
+    ],
   },
 }
 `;
diff --git a/tests/unit/__snapshots__/endpoints-factory.spec.ts.snap b/tests/unit/__snapshots__/endpoints-factory.spec.ts.snap
index 774cd02d..a5bbd1b1 100644
--- a/tests/unit/__snapshots__/endpoints-factory.spec.ts.snap
+++ b/tests/unit/__snapshots__/endpoints-factory.spec.ts.snap
@@ -2,16 +2,32 @@
 
 exports[`EndpointsFactory .build() Should create an endpoint with intersection middleware 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "n1": Object {
-      "_type": "ZodNumber",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodIntersection",
+    "left": Object {
+      "_type": "ZodObject",
+      "shape": Object {
+        "n1": Object {
+          "_type": "ZodNumber",
+        },
+      },
     },
-    "n2": Object {
-      "_type": "ZodNumber",
+    "right": Object {
+      "_type": "ZodObject",
+      "shape": Object {
+        "n2": Object {
+          "_type": "ZodNumber",
+        },
+      },
     },
-    "s": Object {
-      "_type": "ZodString",
+  },
+  "right": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "s": Object {
+        "_type": "ZodString",
+      },
     },
   },
 }
@@ -30,13 +46,21 @@ Object {
 
 exports[`EndpointsFactory .build() Should create an endpoint with simple middleware 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "n": Object {
-      "_type": "ZodNumber",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "n": Object {
+        "_type": "ZodNumber",
+      },
     },
-    "s": Object {
-      "_type": "ZodString",
+  },
+  "right": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "s": Object {
+        "_type": "ZodString",
+      },
     },
   },
 }
@@ -55,22 +79,34 @@ Object {
 
 exports[`EndpointsFactory .build() Should create an endpoint with union middleware 1`] = `
 Object {
-  "_type": "ZodObject",
-  "shape": Object {
-    "n1": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNumber",
+  "_type": "ZodIntersection",
+  "left": Object {
+    "_type": "ZodUnion",
+    "options": Array [
+      Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "n1": Object {
+            "_type": "ZodNumber",
+          },
+        },
       },
-    },
-    "n2": Object {
-      "_type": "ZodOptional",
-      "value": Object {
-        "_type": "ZodNumber",
+      Object {
+        "_type": "ZodObject",
+        "shape": Object {
+          "n2": Object {
+            "_type": "ZodNumber",
+          },
+        },
+      },
+    ],
+  },
+  "right": Object {
+    "_type": "ZodObject",
+    "shape": Object {
+      "s": Object {
+        "_type": "ZodString",
       },
-    },
-    "s": Object {
-      "_type": "ZodString",
     },
   },
 }
diff --git a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
index 3e45bb8c..fc2dbeed 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -633,3 +633,48 @@ Object {
   "type": "object",
 }
 `;
+
+exports[`Open API helpers extractObjectSchema() should pass the object schema through 1`] = `
+Object {
+  "_type": "ZodObject",
+  "shape": Object {
+    "one": Object {
+      "_type": "ZodString",
+    },
+  },
+}
+`;
+
+exports[`Open API helpers extractObjectSchema() should return object schema for the intersection of object schemas 1`] = `
+Object {
+  "_type": "ZodObject",
+  "shape": Object {
+    "one": Object {
+      "_type": "ZodString",
+    },
+    "two": Object {
+      "_type": "ZodNumber",
+    },
+  },
+}
+`;
+
+exports[`Open API helpers extractObjectSchema() should return object schema for the union of object schemas 1`] = `
+Object {
+  "_type": "ZodObject",
+  "shape": Object {
+    "one": Object {
+      "_type": "ZodOptional",
+      "value": Object {
+        "_type": "ZodString",
+      },
+    },
+    "two": Object {
+      "_type": "ZodOptional",
+      "value": Object {
+        "_type": "ZodNumber",
+      },
+    },
+  },
+}
+`;
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 778e7c5d..8d9ba249 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -228,15 +228,25 @@ paths:
           application/json:
             schema:
               description: POST /v1/getSomething request body
-              type: object
-              properties:
-                id:
-                  type: string
-                field1:
-                  type: string
-                field2:
-                  type: string
-              required: []
+              oneOf:
+                - type: object
+                  properties:
+                    id:
+                      type: string
+                    field1:
+                      type: string
+                  required:
+                    - id
+                    - field1
+                - type: object
+                  properties:
+                    id:
+                      type: string
+                    field2:
+                      type: string
+                  required:
+                    - id
+                    - field2
 components:
   schemas: {}
   responses: {}
@@ -328,15 +338,23 @@ paths:
           application/json:
             schema:
               description: POST /v1/getSomething request body
-              type: object
-              properties:
-                key:
-                  type: string
-                str:
-                  type: string
-              required:
-                - key
-                - str
+              allOf:
+                - example:
+                    key: 1234-56789-01
+                  type: object
+                  properties:
+                    key:
+                      type: string
+                  required:
+                    - key
+                - example:
+                    str: test
+                  type: object
+                  properties:
+                    str:
+                      type: string
+                  required:
+                    - str
             examples:
               example1:
                 value:
@@ -1045,41 +1063,36 @@ paths:
                     enum:
                       - success
                   data:
-                    type: object
-                    properties:
-                      switch:
-                        discriminator:
-                          propertyName: status
-                        oneOf:
-                          - type: object
-                            properties:
-                              status:
-                                type: string
-                                enum:
-                                  - success
-                              data:
-                                nullable: true
-                                format: any
-                            required:
-                              - status
-                          - type: object
+                    discriminator:
+                      propertyName: status
+                    oneOf:
+                      - type: object
+                        properties:
+                          status:
+                            type: string
+                            enum:
+                              - success
+                          data:
+                            nullable: true
+                            format: any
+                        required:
+                          - status
+                      - type: object
+                        properties:
+                          status:
+                            type: string
+                            enum:
+                              - error
+                          error:
+                            type: object
                             properties:
-                              status:
+                              message:
                                 type: string
-                                enum:
-                                  - error
-                              error:
-                                type: object
-                                properties:
-                                  message:
-                                    type: string
-                                required:
-                                  - message
                             required:
-                              - status
-                              - error
-                    required:
-                      - switch
+                              - message
+                        required:
+                          - status
+                          - error
                 required:
                   - status
                   - data
@@ -1115,36 +1128,31 @@ paths:
           application/json:
             schema:
               description: POST /v1/getSomething request body
-              type: object
-              properties:
-                switch:
-                  discriminator:
-                    propertyName: type
-                  oneOf:
-                    - type: object
-                      properties:
-                        type:
-                          type: string
-                          enum:
-                            - a
-                        a:
-                          type: string
-                      required:
-                        - type
+              discriminator:
+                propertyName: type
+              oneOf:
+                - type: object
+                  properties:
+                    type:
+                      type: string
+                      enum:
                         - a
-                    - type: object
-                      properties:
-                        type:
-                          type: string
-                          enum:
-                            - b
-                        b:
-                          type: string
-                      required:
-                        - type
+                    a:
+                      type: string
+                  required:
+                    - type
+                    - a
+                - type: object
+                  properties:
+                    type:
+                      type: string
+                      enum:
                         - b
-              required:
-                - switch
+                    b:
+                      type: string
+                  required:
+                    - type
+                    - b
 components:
   schemas: {}
   responses: {}
@@ -1659,25 +1667,34 @@ paths:
           application/json:
             schema:
               description: POST /v1/user/:id request body
-              type: object
-              properties:
-                key:
-                  type: string
-                  minLength: 1
-                name:
-                  type: string
-                  minLength: 1
-                birthday:
-                  type: string
-                  format: date-time
-                  pattern: ^\\\\d{4}-\\\\d{2}-\\\\d{2}(T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{3})?)?Z?$
-                  description: YYYY-MM-DDTHH:mm:ss.sssZ
-                  externalDocs:
-                    url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
-              required:
-                - key
-                - name
-                - birthday
+              allOf:
+                - example:
+                    key: 1234-5678-90
+                  type: object
+                  properties:
+                    key:
+                      type: string
+                      minLength: 1
+                  required:
+                    - key
+                - example:
+                    name: John Doe
+                    birthday: 1963-04-21
+                  type: object
+                  properties:
+                    name:
+                      type: string
+                      minLength: 1
+                    birthday:
+                      type: string
+                      format: date-time
+                      pattern: ^\\\\d{4}-\\\\d{2}-\\\\d{2}(T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{3})?)?Z?$
+                      description: YYYY-MM-DDTHH:mm:ss.sssZ
+                      externalDocs:
+                        url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
+                  required:
+                    - name
+                    - birthday
             examples:
               example1:
                 value:
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index 7106bdb7..42944a07 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -2,10 +2,9 @@ import { UploadedFile } from "express-fileupload";
 import { expectType } from "tsd";
 import {
   combinations,
-  combineEndpointAndMiddlewareInputSchemas,
   defaultInputSources,
-  extractObjectSchema,
   getExamples,
+  getFinalEndpointInputSchema,
   getInitialInput,
   getMessageFromError,
   getRoutePathParams,
@@ -16,19 +15,29 @@ import {
   OutputMarker,
 } from "../../src/common-helpers";
 import {
-  createMiddleware,
   z,
   createHttpError,
   markOutput,
   withMeta,
+  createMiddleware,
 } from "../../src";
 import { Request } from "express";
 import { getMeta } from "../../src/metadata";
-import { MiddlewareDefinition } from "../../src/middleware";
+import { AnyMiddlewareDef } from "../../src/middleware";
 import { serializeSchemaForTest } from "../helpers";
 
 describe("Common Helpers", () => {
-  describe("combineEndpointAndMiddlewareInputSchemas()", () => {
+  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 = [
         createMiddleware({
@@ -49,15 +58,12 @@ describe("Common Helpers", () => {
           }),
           middleware: jest.fn(),
         }),
-      ] as MiddlewareDefinition<any, any, any>[];
+      ] as unknown as AnyMiddlewareDef[];
       const endpointInput = z.object({
         four: z.boolean(),
       });
-      const result = combineEndpointAndMiddlewareInputSchemas(
-        endpointInput,
-        middlewares
-      );
-      expect(result).toBeInstanceOf(z.ZodObject);
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
       expect(serializeSchemaForTest(result)).toMatchSnapshot();
     });
 
@@ -87,7 +93,7 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as MiddlewareDefinition<any, any, any>[];
+      ] as unknown as AnyMiddlewareDef[];
       const endpointInput = z
         .object({
           five: z.string(),
@@ -97,11 +103,8 @@ describe("Common Helpers", () => {
             six: z.number(),
           })
         );
-      const result = combineEndpointAndMiddlewareInputSchemas(
-        endpointInput,
-        middlewares
-      );
-      expect(result).toBeInstanceOf(z.ZodObject);
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
       expect(serializeSchemaForTest(result)).toMatchSnapshot();
     });
 
@@ -131,7 +134,7 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as MiddlewareDefinition<any, any, any>[];
+      ] as unknown as AnyMiddlewareDef[];
       const endpointInput = z
         .object({
           five: z.string(),
@@ -141,11 +144,8 @@ describe("Common Helpers", () => {
             six: z.number(),
           })
         );
-      const result = combineEndpointAndMiddlewareInputSchemas(
-        endpointInput,
-        middlewares
-      );
-      expect(result).toBeInstanceOf(z.ZodObject);
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
       expect(serializeSchemaForTest(result)).toMatchSnapshot();
     });
 
@@ -175,15 +175,12 @@ describe("Common Helpers", () => {
             ),
           middleware: jest.fn(),
         }),
-      ] as MiddlewareDefinition<any, any, any>[];
+      ] as unknown as AnyMiddlewareDef[];
       const endpointInput = z.object({
         five: z.string(),
       });
-      const result = combineEndpointAndMiddlewareInputSchemas(
-        endpointInput,
-        middlewares
-      );
-      expect(result).toBeInstanceOf(z.ZodObject);
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
+      expect(result).toBeInstanceOf(z.ZodIntersection);
       expect(serializeSchemaForTest(result)).toMatchSnapshot();
     });
 
@@ -223,7 +220,7 @@ describe("Common Helpers", () => {
           }),
           middleware: jest.fn(),
         }),
-      ] as MiddlewareDefinition<any, any, any>[];
+      ] as unknown as AnyMiddlewareDef[];
       const endpointInput = withMeta(
         z.object({
           five: z.string(),
@@ -231,10 +228,7 @@ describe("Common Helpers", () => {
       ).example({
         five: "some",
       });
-      const result = combineEndpointAndMiddlewareInputSchemas(
-        endpointInput,
-        middlewares
-      );
+      const result = getFinalEndpointInputSchema(middlewares, endpointInput);
       expect(getMeta(result, "examples")).toEqual([
         {
           one: "test",
@@ -435,110 +429,6 @@ describe("Common Helpers", () => {
     });
   });
 
-  describe("extractObjectSchema()", () => {
-    test("should pass the object schema through", () => {
-      const subject = extractObjectSchema(
-        z.object({
-          one: z.string(),
-        })
-      );
-      expect(subject).toBeInstanceOf(z.ZodObject);
-      expect(serializeSchemaForTest(subject)).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(serializeSchemaForTest(subject)).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(serializeSchemaForTest(subject)).toMatchSnapshot();
-    });
-
-    test("should preserve examples", () => {
-      const objectSchema = withMeta(
-        z.object({
-          one: z.string(),
-        })
-      ).example({
-        one: "test",
-      });
-      expect(getMeta(extractObjectSchema(objectSchema), "examples")).toEqual([
-        {
-          one: "test",
-        },
-      ]);
-
-      const unionSchema = withMeta(
-        z
-          .object({
-            one: z.string(),
-          })
-          .or(
-            z.object({
-              two: z.number(),
-            })
-          )
-      )
-        .example({
-          one: "test1",
-        })
-        .example({
-          two: 123,
-        });
-      expect(getMeta(extractObjectSchema(unionSchema), "examples")).toEqual([
-        { one: "test1" },
-        { two: 123 },
-      ]);
-
-      const intersectionSchema = withMeta(
-        z
-          .object({
-            one: z.string(),
-          })
-          .and(
-            z.object({
-              two: z.number(),
-            })
-          )
-      ).example({
-        one: "test1",
-        two: 123,
-      });
-      expect(
-        getMeta(extractObjectSchema(intersectionSchema), "examples")
-      ).toEqual([
-        {
-          one: "test1",
-          two: 123,
-        },
-      ]);
-    });
-  });
-
   describe("getMessageFromError()", () => {
     test("should compile a string from ZodError", () => {
       const error = new z.ZodError([
diff --git a/tests/unit/endpoints-factory.spec.ts b/tests/unit/endpoints-factory.spec.ts
index 5a4cbced..1bba9131 100644
--- a/tests/unit/endpoints-factory.spec.ts
+++ b/tests/unit/endpoints-factory.spec.ts
@@ -71,7 +71,9 @@ describe("EndpointsFactory", () => {
       expect(factory["resultHandler"]).toStrictEqual(resultHandlerMock);
       expect(newFactory["middlewares"].length).toBe(1);
       expect(newFactory["middlewares"][0].input).toBeInstanceOf(z.ZodObject);
-      expect(newFactory["middlewares"][0].input.shape).toEqual({});
+      expect(
+        (newFactory["middlewares"][0].input as z.AnyZodObject).shape
+      ).toEqual({});
       expect(
         await newFactory["middlewares"][0].middleware({
           input: {},
@@ -107,7 +109,9 @@ describe("EndpointsFactory", () => {
         });
         expect(newFactory["middlewares"].length).toBe(1);
         expect(newFactory["middlewares"][0].input).toBeInstanceOf(z.ZodObject);
-        expect(newFactory["middlewares"][0].input.shape).toEqual({});
+        expect(
+          (newFactory["middlewares"][0].input as z.AnyZodObject).shape
+        ).toEqual({});
         const requestMock = { body: { something: "awesome" } } as Request;
         const responseMock = {} as Response;
         const options = await newFactory["middlewares"][0].middleware({
@@ -251,10 +255,12 @@ describe("EndpointsFactory", () => {
       ).toMatchSnapshot();
       expect(endpoint["handler"]).toStrictEqual(handlerMock);
       expect(endpoint["resultHandler"]).toStrictEqual(resultHandlerMock);
-      expectType<{
-        n: z.ZodNumber;
-        s: z.ZodString;
-      }>(endpoint["inputSchema"].shape);
+      expectType<
+        z.ZodIntersection<
+          z.ZodObject<{ n: z.ZodNumber }>,
+          z.ZodObject<{ s: z.ZodString }>
+        >
+      >(endpoint.getInputSchema());
     });
 
     test("Should create an endpoint with intersection middleware", () => {
@@ -294,11 +300,15 @@ describe("EndpointsFactory", () => {
       ).toMatchSnapshot();
       expect(endpoint["handler"]).toStrictEqual(handlerMock);
       expect(endpoint["resultHandler"]).toStrictEqual(resultHandlerMock);
-      expectType<{
-        n1: z.ZodNumber;
-        n2: z.ZodNumber;
-        s: z.ZodString;
-      }>(endpoint["inputSchema"].shape);
+      expectType<
+        z.ZodIntersection<
+          z.ZodIntersection<
+            z.ZodObject<{ n1: z.ZodNumber }>,
+            z.ZodObject<{ n2: z.ZodNumber }>
+          >,
+          z.ZodObject<{ s: z.ZodString }>
+        >
+      >(endpoint.getInputSchema());
     });
 
     test("Should create an endpoint with union middleware", () => {
@@ -342,22 +352,13 @@ describe("EndpointsFactory", () => {
       expect(endpoint["handler"]).toStrictEqual(handlerMock);
       expect(endpoint["resultHandler"]).toStrictEqual(resultHandlerMock);
       expectType<
-        (
-          | {
-              n1: z.ZodNumber;
-            }
-          | {
-              n2: z.ZodNumber;
-            }
-        ) & {
-          s: z.ZodString;
-        }
-      >(endpoint["inputSchema"].shape);
-      expectType<{
-        n1?: z.ZodNumber;
-        n2?: z.ZodNumber;
-        s: z.ZodString;
-      }>(endpoint["inputSchema"].shape);
+        z.ZodIntersection<
+          z.ZodUnion<
+            [z.ZodObject<{ n1: z.ZodNumber }>, z.ZodObject<{ n2: z.ZodNumber }>]
+          >,
+          z.ZodObject<{ s: z.ZodString }>
+        >
+      >(endpoint.getInputSchema());
     });
   });
 });
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 7d9e05b2..954b3609 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -4,6 +4,7 @@ import {
   withMeta,
   z,
 } from "../../src/index";
+import { getMeta } from "../../src/metadata";
 import {
   depictAny,
   depictArray,
@@ -36,9 +37,115 @@ import {
   excludeExampleFromDepiction,
   excludeParamsFromDepiction,
   reformatParamsInPath,
+  extractObjectSchema,
 } from "../../src/open-api-helpers";
+import { serializeSchemaForTest } from "../helpers";
 
 describe("Open API helpers", () => {
+  describe("extractObjectSchema()", () => {
+    test("should pass the object schema through", () => {
+      const subject = extractObjectSchema(
+        z.object({
+          one: z.string(),
+        })
+      );
+      expect(subject).toBeInstanceOf(z.ZodObject);
+      expect(serializeSchemaForTest(subject)).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(serializeSchemaForTest(subject)).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(serializeSchemaForTest(subject)).toMatchSnapshot();
+    });
+
+    test("should preserve examples", () => {
+      const objectSchema = withMeta(
+        z.object({
+          one: z.string(),
+        })
+      ).example({
+        one: "test",
+      });
+      expect(getMeta(extractObjectSchema(objectSchema), "examples")).toEqual([
+        {
+          one: "test",
+        },
+      ]);
+
+      const unionSchema = withMeta(
+        z
+          .object({
+            one: z.string(),
+          })
+          .or(
+            z.object({
+              two: z.number(),
+            })
+          )
+      )
+        .example({
+          one: "test1",
+        })
+        .example({
+          two: 123,
+        });
+      expect(getMeta(extractObjectSchema(unionSchema), "examples")).toEqual([
+        { one: "test1" },
+        { two: 123 },
+      ]);
+
+      const intersectionSchema = withMeta(
+        z
+          .object({
+            one: z.string(),
+          })
+          .and(
+            z.object({
+              two: z.number(),
+            })
+          )
+      ).example({
+        one: "test1",
+        two: 123,
+      });
+      expect(
+        getMeta(extractObjectSchema(intersectionSchema), "examples")
+      ).toEqual([
+        {
+          one: "test1",
+          two: 123,
+        },
+      ]);
+    });
+  });
+
   describe("excludeParamsFromDepiction()", () => {
     test("should omit specified path params", () => {
       const depicted = depictSchema({
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index 1243790d..1fd4c4d1 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -168,7 +168,6 @@ describe("Open API generator", () => {
       expect(spec).toMatchSnapshot();
     });
 
-    // @todo move it to the top level
     test("should generate the correct schema for discriminated union type", () => {
       const spec = new OpenAPI({
         config,
@@ -176,26 +175,20 @@ describe("Open API generator", () => {
           v1: {
             getSomething: defaultEndpointsFactory.build({
               methods: ["post"],
-              input: z.object({
-                switch: z.discriminatedUnion("type", [
-                  z.object({ type: z.literal("a"), a: z.string() }),
-                  z.object({ type: z.literal("b"), b: z.string() }),
-                ]),
-              }),
-              output: z.object({
-                switch: z.discriminatedUnion("status", [
-                  z.object({ status: z.literal("success"), data: z.any() }),
-                  z.object({
-                    status: z.literal("error"),
-                    error: z.object({ message: z.string() }),
-                  }),
-                ]),
-              }),
+              input: z.discriminatedUnion("type", [
+                z.object({ type: z.literal("a"), a: z.string() }),
+                z.object({ type: z.literal("b"), b: z.string() }),
+              ]),
+              output: z.discriminatedUnion("status", [
+                z.object({ status: z.literal("success"), data: z.any() }),
+                z.object({
+                  status: z.literal("error"),
+                  error: z.object({ message: z.string() }),
+                }),
+              ]),
               handler: async () => ({
-                switch: {
-                  status: "success" as const,
-                  data: "test",
-                },
+                status: "success" as const,
+                data: "test",
               }),
             }),
           },