Repository: RobinTail/express-zod-api
Pull request #83 (2021-07-24 21:14:30)

Zod file type
Based on the discussion https://github.com/RobinTail/express-zod-api/discussions/78
I'm making a custom schema for the file streaming response: `z.file()` with optional refinements `.binary()` and `.base64()`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e2db0cb..79051dfc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
 
 ## Version 2
 
+### v2.1.0
+
+- New response schema type `ZodFile` can be created using `z.file()`. It has two refinements: `.binary()` and 
+  `.base64()` which also reflected in the generated Swagger / OpenAPI documentation.
+  You can use it instead of `z.string()` with `createApiResponse()`:
+```typescript
+// before
+const fileStreamingEndpointsFactoryBefore = new EndpointsFactory(createResultHandler({
+  getPositiveResponse: () => createApiResponse(z.string(), 'image/*'),
+  ...
+}));
+
+// after
+const fileStreamingEndpointsFactoryAfter = new EndpointsFactory(createResultHandler({
+  getPositiveResponse: () => createApiResponse(z.file().binary(), 'image/*'),
+  ...
+}));
+```
+
 ### v2.0.0
 
 - First stable release of the v2.
diff --git a/README.md b/README.md
index 88cd952c..aab018ca 100644
--- a/README.md
+++ b/README.md
@@ -308,11 +308,14 @@ image file including its MIME type in `Content-type` header.
 
 You can find two approaches to `EndpointsFactory` and `ResultHandler` implementation 
 [in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts). 
-One of them implements file streaming, in this case the endpoint just has to provide the filename:
+One of them implements file streaming, in this case the endpoint just has to provide the filename.
+The response schema generally may be just `z.string()`, but there is also a specific one: `z.file()` that also supports
+`.binary()` and `.base64()` refinements which are reflected in the 
+[generated documentation](#swagger--openapi-specification).
 
 ```typescript
 const fileStreamingEndpointsFactory = new EndpointsFactory(createResultHandler({
-  getPositiveResponse: () => createApiResponse(z.string(), 'image/*'),
+  getPositiveResponse: () => createApiResponse(z.file().binary(), 'image/*'),
   getNegativeResponse: () => createApiResponse(z.string(), 'text/plain'),
   handler: ({response, error, output}) => {
     if (error) {
diff --git a/coverage.svg b/coverage.svg
index c17c5efb..971807e7 100644
--- a/coverage.svg
+++ b/coverage.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 98.06%"><title>Coverage: 98.06%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">98.06%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">98.06%</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 98.24%"><title>Coverage: 98.24%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">98.24%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">98.24%</text></g></svg>
\ No newline at end of file
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index a9245434..09998fa1 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -156,6 +156,7 @@ paths:
             image/*:
               schema:
                 type: string
+                format: binary
         "400":
           description: GET /v1/stream Error response
           content:
diff --git a/example/factories.ts b/example/factories.ts
index 72b4c231..ef23bd60 100644
--- a/example/factories.ts
+++ b/example/factories.ts
@@ -28,7 +28,7 @@ export const fileDownloadEndpointsFactory = new EndpointsFactory(createResultHan
 }));
 
 export const fileStreamingEndpointsFactory = new EndpointsFactory(createResultHandler({
-  getPositiveResponse: () => createApiResponse(z.string(), 'image/*'),
+  getPositiveResponse: () => createApiResponse(z.file().binary(), 'image/*'),
   getNegativeResponse: () => createApiResponse(z.string(), lookup('txt')),
   handler: ({response, error, output}) => {
     if (error) {
diff --git a/src/extend-zod.ts b/src/extend-zod.ts
new file mode 100644
index 00000000..bb0072e6
--- /dev/null
+++ b/src/extend-zod.ts
@@ -0,0 +1,4 @@
+import {ZodFile} from './file-schema';
+
+export * from 'zod';
+export const file = ZodFile.create;
diff --git a/src/file-schema.ts b/src/file-schema.ts
new file mode 100644
index 00000000..46466d31
--- /dev/null
+++ b/src/file-schema.ts
@@ -0,0 +1,96 @@
+import {
+  ParseContext,
+  ParseReturnType,
+  ZodIssueCode,
+  ZodParsedType,
+  ZodType,
+  INVALID,
+  OK,
+  ZodString,
+} from 'zod';
+
+const zodFileKind = 'ZodFile';
+
+// obtaining the private helper type from Zod
+type ErrMessage = Exclude<Parameters<typeof ZodString.prototype.email>[0], undefined>;
+
+// the copy of the private Zod errorUtil.errToObj
+const errToObj = (message: ErrMessage | undefined) => typeof message === 'string' ? {message} : message || {};
+
+declare type ZodFileCheck = {
+  kind: 'binary';
+  message?: string;
+} | {
+  kind: 'base64';
+  message?: string;
+};
+
+export interface ZodFileDef {
+  checks: ZodFileCheck[];
+  typeName: typeof zodFileKind;
+}
+
+const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
+
+export class ZodFile extends ZodType<string, ZodFileDef> {
+  _parse(
+    ctx: ParseContext,
+    data: any,
+    parsedType: ZodParsedType
+  ): ParseReturnType<string> {
+    if (parsedType !== ZodParsedType.string) {
+      ctx.addIssue(data, {
+        code: ZodIssueCode.invalid_type,
+        expected: ZodParsedType.string,
+        received: parsedType,
+      });
+      return INVALID;
+    }
+    let invalid = false;
+
+    for (const check of this._def.checks) {
+      if (check.kind === 'base64') {
+        if (!base64Regex.test(data)) {
+          invalid = true;
+          ctx.addIssue(data, {
+            code: ZodIssueCode.custom,
+            message: check.message,
+          });
+        }
+      }
+    }
+
+    return invalid ? INVALID : OK(data);
+  }
+
+  binary = (message?: ErrMessage) =>
+    new ZodFile({
+      ...this._def,
+      checks: [
+        ...this._def.checks,
+        { kind: 'binary', ...errToObj(message) },
+      ],
+    });
+
+  base64 = (message?: ErrMessage) =>
+    new ZodFile({
+      ...this._def,
+      checks: [
+        ...this._def.checks,
+        { kind: 'base64', ...errToObj(message) },
+      ],
+    });
+
+  get isBinary() {
+    return !!this._def.checks.find((check) => check.kind === 'binary');
+  }
+
+  get isBase64() {
+    return !!this._def.checks.find((check) => check.kind === 'base64');
+  }
+
+  static create = () => new ZodFile({
+    checks: [],
+    typeName: zodFileKind
+  });
+}
diff --git a/src/helpers.ts b/src/helpers.ts
index c23c1bac..6d0c18ac 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -6,7 +6,6 @@ import {LoggerConfig, loggerLevels} from './config-type';
 import {MiddlewareDefinition} from './middleware';
 
 export type FlatObject = Record<string, any>;
-// type EmptyFlatObject = {[K in never]: never};
 
 type ObjectSchema = z.AnyZodObject;
 type Extractable = 'shape' | '_unknownKeys' | '_catchall' | '_output' | '_input';
@@ -24,7 +23,7 @@ type IntersectionResult<T extends IntersectionSchema, F extends Extractable> =
 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; // : EmptyFlatObject
+  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'>,
diff --git a/src/index.ts b/src/index.ts
index 0439ba4d..5d56f72a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,7 +11,7 @@ export { createServer, attachRouting } from './server';
 export { OpenAPI } from './open-api';
 export { OpenAPIError, DependsOnMethodError, RoutingError } from './errors';
 
-import { z } from 'zod';
+import * as z from './extend-zod';
 import createHttpError from 'http-errors';
 
 export { createHttpError, z };
diff --git a/src/open-api.ts b/src/open-api.ts
index 269520c8..b0e401c2 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -7,6 +7,7 @@ import {
 } from 'openapi3-ts';
 import {z} from 'zod';
 import {OpenAPIError} from './errors';
+import {ZodFile} from './file-schema';
 import {extractObjectSchema} from './helpers';
 import {Routing, routingCycle, RoutingCycleParams} from './routing';
 import {lookup} from 'mime';
@@ -29,7 +30,6 @@ const describeSchema = (value: z.ZodTypeAny, isResponse: boolean): SchemaObject
       return {...otherProps, type: 'string', format: 'date'};
     case value instanceof z.ZodNull:
       // null is not supported https://swagger.io/docs/specification/data-models/data-types/
-      // return {...otherProps, type: 'null'};
       return {...otherProps, type: 'string', nullable: true, format: 'null'};
     case value instanceof z.ZodArray:
       return {
@@ -85,6 +85,13 @@ const describeSchema = (value: z.ZodTypeAny, isResponse: boolean): SchemaObject
         oneOf: (value as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>)._def.options
           .map((schema) => describeSchema(schema, isResponse))
       };
+    case value instanceof ZodFile:
+      return {
+        ...otherProps,
+        type: 'string',
+        format: (value as ZodFile).isBinary ? 'binary' :
+          (value as ZodFile).isBase64 ? 'byte' : 'file'
+      };
     case value instanceof z.ZodUndefined:
     case value instanceof z.ZodTuple:
     case value instanceof z.ZodMap:


Test patch:
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 9e101eff..9cd268ad 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -264,6 +264,104 @@ servers:
 "
 `;
 
+exports[`Open API generator generateOpenApi() should generate the correct schema for union type 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing Union and Or Types
+  version: 3.4.5
+paths:
+  /v1/getSomething:
+    post:
+      responses:
+        \\"200\\":
+          description: POST /v1/getSomething Successful response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - success
+                  data:
+                    type: object
+                    properties:
+                      or:
+                        oneOf:
+                          - type: string
+                          - type: number
+                    required:
+                      - or
+                required:
+                  - status
+                  - data
+        \\"400\\":
+          description: POST /v1/getSomething Error response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - error
+                  error:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+                    required:
+                      - message
+                required:
+                  - status
+                  - error
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                union:
+                  oneOf:
+                    - type: object
+                      properties:
+                        one:
+                          type: string
+                        two:
+                          type: number
+                      required:
+                        - one
+                        - two
+                    - type: object
+                      properties:
+                        two:
+                          type: number
+                        three:
+                          type: string
+                      required:
+                        - two
+                        - three
+              required:
+                - union
+              description: POST /v1/getSomething request body
+components:
+  schemas: {}
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes: {}
+  links: {}
+  callbacks: {}
+tags: []
+servers:
+  - url: http://example.com
+"
+`;
+
 exports[`Open API generator generateOpenApi() should generate the correct schema of example routing 1`] = `
 "openapi: 3.0.0
 info:
@@ -423,6 +521,7 @@ paths:
             image/*:
               schema:
                 type: string
+                format: binary
         \\"400\\":
           description: GET /v1/stream Error response
           content:
@@ -452,105 +551,7 @@ servers:
 "
 `;
 
-exports[`Open API generator should generate the correct schema for union type 1`] = `
-"openapi: 3.0.0
-info:
-  title: Testing Union and Or Types
-  version: 3.4.5
-paths:
-  /v1/getSomething:
-    post:
-      responses:
-        \\"200\\":
-          description: POST /v1/getSomething Successful response
-          content:
-            application/json:
-              schema:
-                type: object
-                properties:
-                  status:
-                    type: string
-                    enum:
-                      - success
-                  data:
-                    type: object
-                    properties:
-                      or:
-                        oneOf:
-                          - type: string
-                          - type: number
-                    required:
-                      - or
-                required:
-                  - status
-                  - data
-        \\"400\\":
-          description: POST /v1/getSomething Error response
-          content:
-            application/json:
-              schema:
-                type: object
-                properties:
-                  status:
-                    type: string
-                    enum:
-                      - error
-                  error:
-                    type: object
-                    properties:
-                      message:
-                        type: string
-                    required:
-                      - message
-                required:
-                  - status
-                  - error
-      requestBody:
-        content:
-          application/json:
-            schema:
-              type: object
-              properties:
-                union:
-                  oneOf:
-                    - type: object
-                      properties:
-                        one:
-                          type: string
-                        two:
-                          type: number
-                      required:
-                        - one
-                        - two
-                    - type: object
-                      properties:
-                        two:
-                          type: number
-                        three:
-                          type: string
-                      required:
-                        - two
-                        - three
-              required:
-                - union
-              description: POST /v1/getSomething request body
-components:
-  schemas: {}
-  responses: {}
-  parameters: {}
-  examples: {}
-  requestBodies: {}
-  headers: {}
-  securitySchemes: {}
-  links: {}
-  callbacks: {}
-tags: []
-servers:
-  - url: http://example.com
-"
-`;
-
-exports[`Open API generator should handle bigint, boolean, date and null 1`] = `
+exports[`Open API generator generateOpenApi() should handle bigint, boolean, date and null 1`] = `
 "openapi: 3.0.0
 info:
   title: Testing additional types
@@ -638,7 +639,7 @@ servers:
 "
 `;
 
-exports[`Open API generator should handle enum types 1`] = `
+exports[`Open API generator generateOpenApi() should handle enum types 1`] = `
 "openapi: 3.0.0
 info:
   title: Testing enums
@@ -722,7 +723,7 @@ servers:
 "
 `;
 
-exports[`Open API generator should handle transformation schema in output 1`] = `
+exports[`Open API generator generateOpenApi() should handle transformation schema in output 1`] = `
 "openapi: 3.0.0
 info:
   title: Testing Transformation in response schema
diff --git a/tests/unit/file-schema.spec.ts b/tests/unit/file-schema.spec.ts
new file mode 100644
index 00000000..2eacb20e
--- /dev/null
+++ b/tests/unit/file-schema.spec.ts
@@ -0,0 +1,109 @@
+import {createRootContext, getParsedType} from 'zod';
+import {ZodFile} from '../../src/file-schema';
+import fs from 'fs';
+
+describe('ZodFile', () => {
+  describe('static::create()', () => {
+    test('should create an instance', () => {
+      const schema = ZodFile.create();
+      expect(schema).toBeInstanceOf(ZodFile);
+      expect(schema._def.checks).toEqual([]);
+      expect(schema._def.typeName).toEqual('ZodFile');
+      expect(schema.isBinary).toBeFalsy();
+      expect(schema.isBase64).toBeFalsy();
+    });
+  });
+
+  describe('.binary()', () => {
+    test('should create a binary file', () => {
+      const schema = ZodFile.create().binary('test message');
+      expect(schema).toBeInstanceOf(ZodFile);
+      expect(schema.isBinary).toBeTruthy();
+      expect(schema._def.checks).toEqual([{
+        kind: 'binary',
+        message: 'test message'
+      }]);
+    });
+  });
+
+  describe('.base64()', () => {
+    test('should create a base64 file', () => {
+      const schema = ZodFile.create().base64('test message');
+      expect(schema).toBeInstanceOf(ZodFile);
+      expect(schema.isBase64).toBeTruthy();
+      expect(schema._def.checks).toEqual([{
+        kind: 'base64',
+        message: 'test message'
+      }]);
+    });
+  });
+
+  describe('_parse()', () => {
+    test('should handle wrong parsed type', () => {
+      const context = createRootContext({});
+      const schema = ZodFile.create();
+      const result = schema._parse(context, 123, 'number');
+      expect(result).toEqual({
+        valid: false
+      });
+      expect(context.issues).toEqual([{
+        code: 'invalid_type',
+        expected: 'string',
+        message: 'Expected string, received number',
+        path: [],
+        received: 'number',
+      }]);
+    });
+
+    test('should perform additional check for base64 file', () => {
+      const context = createRootContext({});
+      const schema = ZodFile.create().base64('this is not base64');
+      const result = schema._parse(context, '~~~~', 'string');
+      expect(result).toEqual({
+        valid: false
+      });
+      expect(context.issues).toEqual([{
+        code: 'custom',
+        message: 'this is not base64',
+        path: [],
+      }]);
+    });
+
+    test('should accept string', () => {
+      const context = createRootContext({});
+      const schema = ZodFile.create();
+      const result = schema._parse(context, 'some string', 'string');
+      expect(result).toEqual({
+        valid: true,
+        value: 'some string'
+      });
+      expect(context.issues).toEqual([]);
+    });
+
+    test('should accept binary read string', () => {
+      const context = createRootContext({});
+      const schema = ZodFile.create().binary();
+      const data = fs.readFileSync('logo.svg', 'binary');
+      const type = getParsedType(data);
+      const result = schema._parse(context, data, type);
+      expect(result).toEqual({
+        valid: true,
+        value: data
+      });
+      expect(context.issues).toEqual([]);
+    });
+
+    test('should accept base64 read string', () => {
+      const context = createRootContext({});
+      const schema = ZodFile.create().base64();
+      const data = fs.readFileSync('logo.svg', 'base64');
+      const type = getParsedType(data);
+      const result = schema._parse(context, data, type);
+      expect(result).toEqual({
+        valid: true,
+        value: data
+      });
+      expect(context.issues).toEqual([]);
+    });
+  });
+});
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index ab02c9af..6b759ac2 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -105,150 +105,150 @@ describe('Open API generator', () => {
       }).getSpecAsYaml();
       expect(spec).toMatchSnapshot();
     });
-  });
-
-  test('should generate the correct schema for union type', () => {
-    const spec = new OpenAPI({
-      routing: {
-        v1: {
-          getSomething: defaultEndpointsFactory.build({
-            methods: ['post'],
-            input: z.object({
-              union: z.union([
-                z.object({
-                  one: z.string(),
-                  two: z.number()
-                }),
-                z.object({
-                  two: z.number(),
-                  three: z.string()
-                })
-              ]),
-            }),
-            output: z.object({
-              or: z.string().or(z.number()),
-            }),
-            handler: async () => ({
-              or: 554
-            })
-          })
-        }
-      },
-      version: '3.4.5',
-      title: 'Testing Union and Or Types',
-      serverUrl: 'http://example.com'
-    }).getSpecAsYaml();
-    expect(spec).toMatchSnapshot();
-  });
 
-  test('should handle transformation schema in output', () => {
-    const spec = new OpenAPI({
-      routing: {
-        v1: {
-          getSomething: defaultEndpointsFactory.build({
-            methods: ['post'],
-            input: z.object({
-              one: z.string(),
-              two: z.number()
-            }),
-            output: z.object({
-              transform: z.string().transform((str) => str.length)
-            }),
-            handler: async () => ({
-              transform: 'test'
+    test('should generate the correct schema for union type', () => {
+      const spec = new OpenAPI({
+        routing: {
+          v1: {
+            getSomething: defaultEndpointsFactory.build({
+              methods: ['post'],
+              input: z.object({
+                union: z.union([
+                  z.object({
+                    one: z.string(),
+                    two: z.number()
+                  }),
+                  z.object({
+                    two: z.number(),
+                    three: z.string()
+                  })
+                ]),
+              }),
+              output: z.object({
+                or: z.string().or(z.number()),
+              }),
+              handler: async () => ({
+                or: 554
+              })
             })
-          })
-        }
-      },
-      version: '3.4.5',
-      title: 'Testing Transformation in response schema',
-      serverUrl: 'http://example.com'
-    }).getSpecAsYaml();
-    expect(spec).toMatchSnapshot();
-  });
+          }
+        },
+        version: '3.4.5',
+        title: 'Testing Union and Or Types',
+        serverUrl: 'http://example.com'
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
 
-  test('should handle bigint, boolean, date and null', () => {
-    const spec = new OpenAPI({
-      routing: {
-        v1: {
-          getSomething: defaultEndpointsFactory.build({
-            method: 'post',
-            input: z.object({
-              bigint: z.bigint(),
-              boolean: z.boolean(),
-              date: z.date()
-            }),
-            output: z.object({
-              null: z.null()
-            }),
-            handler: async () => ({
-              null: null
+    test('should handle transformation schema in output', () => {
+      const spec = new OpenAPI({
+        routing: {
+          v1: {
+            getSomething: defaultEndpointsFactory.build({
+              methods: ['post'],
+              input: z.object({
+                one: z.string(),
+                two: z.number()
+              }),
+              output: z.object({
+                transform: z.string().transform((str) => str.length)
+              }),
+              handler: async () => ({
+                transform: 'test'
+              })
             })
-          })
-        }
-      },
-      version: '3.4.5',
-      title: 'Testing additional types',
-      serverUrl: 'http://example.com'
-    }).getSpecAsYaml();
-    expect(spec).toMatchSnapshot();
-  });
+          }
+        },
+        version: '3.4.5',
+        title: 'Testing Transformation in response schema',
+        serverUrl: 'http://example.com'
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
 
-  test('should handle enum types', () => {
-    const spec = new OpenAPI({
-      routing: {
-        v1: {
-          getSomething: defaultEndpointsFactory.build({
-            method: 'post',
-            input: z.object({
-              regularEnum: z.enum(['ABC', 'DEF']),
-            }),
-            output: z.object({
-              nativeEnum: z.nativeEnum({FEG: 1, XYZ: 2})
-            }),
-            handler: async () => ({
-              nativeEnum: 1
+    test('should handle bigint, boolean, date and null', () => {
+      const spec = new OpenAPI({
+        routing: {
+          v1: {
+            getSomething: defaultEndpointsFactory.build({
+              method: 'post',
+              input: z.object({
+                bigint: z.bigint(),
+                boolean: z.boolean(),
+                date: z.date()
+              }),
+              output: z.object({
+                null: z.null()
+              }),
+              handler: async () => ({
+                null: null
+              })
             })
-          })
-        }
-      },
-      version: '3.4.5',
-      title: 'Testing enums',
-      serverUrl: 'http://example.com'
-    }).getSpecAsYaml();
-    expect(spec).toMatchSnapshot();
-  });
+          }
+        },
+        version: '3.4.5',
+        title: 'Testing additional types',
+        serverUrl: 'http://example.com'
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
 
-  test('should throw on unsupported types', () => {
-    [
-      z.undefined(),
-      z.tuple([]),
-      z.map(z.any(), z.any()),
-      z.function(),
-      z.lazy(() => z.any()),
-      z.promise(z.any()),
-      z.any(),
-      z.unknown(),
-      z.never(),
-      z.void()
-    ].forEach((zodType) => {
-      expect(() => new OpenAPI({
+    test('should handle enum types', () => {
+      const spec = new OpenAPI({
         routing: {
           v1: {
             getSomething: defaultEndpointsFactory.build({
               method: 'post',
               input: z.object({
-                property: zodType
+                regularEnum: z.enum(['ABC', 'DEF']),
               }),
-              output: z.object({}),
-              handler: async () => ({})
+              output: z.object({
+                nativeEnum: z.nativeEnum({FEG: 1, XYZ: 2})
+              }),
+              handler: async () => ({
+                nativeEnum: 1
+              })
             })
           }
         },
         version: '3.4.5',
-        title: 'Testing unsupported types',
+        title: 'Testing enums',
         serverUrl: 'http://example.com'
-      })).toThrowError(/Zod type Zod\w+ is unsupported/);
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
+
+    test('should throw on unsupported types', () => {
+      [
+        z.undefined(),
+        z.tuple([]),
+        z.map(z.any(), z.any()),
+        z.function(),
+        z.lazy(() => z.any()),
+        z.promise(z.any()),
+        z.any(),
+        z.unknown(),
+        z.never(),
+        z.void()
+      ].forEach((zodType) => {
+        expect(() => new OpenAPI({
+          routing: {
+            v1: {
+              getSomething: defaultEndpointsFactory.build({
+                method: 'post',
+                input: z.object({
+                  property: zodType
+                }),
+                output: z.object({}),
+                handler: async () => ({})
+              })
+            }
+          },
+          version: '3.4.5',
+          title: 'Testing unsupported types',
+          serverUrl: 'http://example.com'
+        })).toThrowError(/Zod type Zod\w+ is unsupported/);
+      });
     });
   });
 });