Repository: RobinTail/express-zod-api
Pull request #297 (2022-01-22 17:18:28)

Dates problem resolution
JSON does not support Date.
Using `z.date()` in IO is confusional.

RobinTail: ✅ Approved for beta
RobinTail: 🚀 Released as 5.5.0-beta1
RobinTail: Tested — operational
https://github.com/RobinTail/incaseofmydeath/commit/39c0af48b7fa97da0c80022983b083132ce0152d

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2d6c204..b50f3cad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,46 @@
 
 ## Version 5
 
+### v5.5.0
+
+- No changes.
+
+### v5.5.0-beta1
+
+- `z.date()` is deprecated for using within IO schemas of your API.
+- Feature #297: `z.dateIn()` and `z.dateOut()` schemas.
+  - Since `Date` cannot be passed directly in JSON format, attempting to return `Date` from the endpoint handler
+    results in it being converted to an ISO `string` in actual response. It is also impossible to transmit the `Date`
+    in its original form to your endpoints within JSON. Therefore, there is confusion with original method `z.date()`.
+  - In order to solve this problem, the library provides two custom methods for dealing with dates: `z.dateIn()` and
+    `z.dateOut()` for using within input and output schemas accordingly.
+  - `z.dateIn()` is a transforming schema that accepts an ISO `string` representation of a `Date`, validates it, and
+    provides your endpoint handler or middleware with a `Date`.
+  - `z.dateOut()`, on the contrary, accepts a `Date` and provides `ResultHanlder` with a `string` representation in ISO
+    format for the response transmission.
+
+```typescript
+import { z, defaultEndpointsFactory } from "express-zod-api";
+
+const updateUserEndpoint = defaultEndpointsFactory.build({
+  method: "post",
+  input: z.object({
+    userId: z.string(),
+    birthday: z.dateIn(), // string -> Date
+  }),
+  output: z.object({
+    createdAt: z.dateOut(), // Date -> string
+  }),
+  handler: async ({ input }) => {
+    // input.birthday is Date
+    return {
+      // transmitted as "2022-01-22T00:00:00.000Z"
+      createdAt: new Date("2022-01-22"),
+    };
+  },
+});
+```
+
 ### v5.4.2
 
 - `ramda` version is 0.28.0.
diff --git a/README.md b/README.md
index f9b936b0..5a3a063d 100644
--- a/README.md
+++ b/README.md
@@ -30,19 +30,20 @@ Start your API server with I/O schema validation and custom middlewares in minut
    2. [Options](#options)
    3. [Refinements](#refinements)
    4. [Transformations](#transformations)
-   5. [Route path params](#route-path-params)
-   6. [Response customization](#response-customization)
-   7. [Non-object response](#non-object-response) including file downloads
-   8. [File uploads](#file-uploads)
-   9. [Customizing logger](#customizing-logger)
-   10. [Connect to your own express app](#connect-to-your-own-express-app)
-   11. [Multiple schemas for one route](#multiple-schemas-for-one-route)
-   12. [Serving static files](#serving-static-files)
-   13. [Customizing input sources](#customizing-input-sources)
-   14. [Enabling compression](#enabling-compression)
-   15. [Enabling HTTPS](#enabling-https)
-   16. [Informing the frontend about the API](#informing-the-frontend-about-the-api)
-   17. [Creating a documentation](#creating-a-documentation)
+   5. [Dealing with dates](#dealing-with-dates)
+   6. [Route path params](#route-path-params)
+   7. [Response customization](#response-customization)
+   8. [Non-object response](#non-object-response) including file downloads
+   9. [File uploads](#file-uploads)
+   10. [Customizing logger](#customizing-logger)
+   11. [Connect to your own express app](#connect-to-your-own-express-app)
+   12. [Multiple schemas for one route](#multiple-schemas-for-one-route)
+   13. [Serving static files](#serving-static-files)
+   14. [Customizing input sources](#customizing-input-sources)
+   15. [Enabling compression](#enabling-compression)
+   16. [Enabling HTTPS](#enabling-https)
+   17. [Informing the frontend about the API](#informing-the-frontend-about-the-api)
+   18. [Creating a documentation](#creating-a-documentation)
 5. [Additional hints](#additional-hints)
    1. [How to test endpoints](#how-to-test-endpoints)
    2. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
@@ -332,6 +333,55 @@ const getUserEndpoint = endpointsFactory.build({
 });
 ```
 
+## Dealing with dates
+
+Dates in Javascript are one of the most troublesome entities. In addition, `Date` cannot be passed directly in JSON
+format. Therefore, attempting to return `Date` from the endpoint handler results in it being converted to an ISO string
+in actual response by calling
+[toJSON()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON),
+which in turn calls
+[toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
+It is also impossible to transmit the `Date` in its original form to your endpoints within JSON. Therefore, there is
+confusion with original method `z.date()` that should not be used within IO schemas of your API.
+
+In order to solve this problem, the library provides two custom methods for dealing with dates: `z.dateIn()` and
+`z.dateOut()` for using within input and output schemas accordingly.
+
+`z.dateIn()` is a transforming schema that accepts an ISO `string` representation of a `Date`, validates it, and
+provides your endpoint handler or middleware with a `Date`. It supports the following formats:
+
+```text
+2021-12-31T23:59:59.000Z
+2021-12-31T23:59:59Z
+2021-12-31T23:59:59
+2021-12-31
+```
+
+`z.dateOut()`, on the contrary, accepts a `Date` and provides `ResultHanlder` with a `string` representation in ISO
+format for the response transmission. Consider the following simplified example for better understanding:
+
+```typescript
+import { z, defaultEndpointsFactory } from "express-zod-api";
+
+const updateUserEndpoint = defaultEndpointsFactory.build({
+  method: "post",
+  input: z.object({
+    userId: z.string(),
+    birthday: z.dateIn(), // string -> Date
+  }),
+  output: z.object({
+    createdAt: z.dateOut(), // Date -> string
+  }),
+  handler: async ({ input }) => {
+    // input.birthday is Date
+    return {
+      // transmitted as "2022-01-22T00:00:00.000Z"
+      createdAt: new Date("2022-01-22"),
+    };
+  },
+});
+```
+
 ## Route path params
 
 You can describe the route of the endpoint using parameters:
diff --git a/example/endpoints/update-user.ts b/example/endpoints/update-user.ts
index 808c887a..f00705e4 100644
--- a/example/endpoints/update-user.ts
+++ b/example/endpoints/update-user.ts
@@ -16,19 +16,21 @@ export const updateUserEndpoint =
             "should be greater than or equal to 0"
           ),
         name: z.string().nonempty(),
+        birthday: z.dateIn(),
       })
     ).example({
       id: "12",
       name: "John Doe",
+      birthday: "1963-04-21",
     }),
     output: withMeta(
       z.object({
         name: z.string(),
-        timestamp: z.number().int().nonnegative(),
+        createdAt: z.dateOut(),
       })
     ).example({
       name: "John Doe",
-      timestamp: 1235698995125,
+      createdAt: new Date("2021-12-31"),
     }),
     handler: async ({
       input: { id, name, key },
@@ -40,7 +42,7 @@ export const updateUserEndpoint =
         throw createHttpError(404, "User not found");
       }
       return {
-        timestamp: Date.now(),
+        createdAt: new Date("2022-01-22"),
         name,
       };
     },
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index c1186ed5..70bb2174 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.4.2
+  version: 5.5.0-beta1
 paths:
   /v1/user/retrieve:
     get:
@@ -88,21 +88,20 @@ paths:
                   data:
                     example:
                       name: John Doe
-                      timestamp: 1235698995125
+                      createdAt: 2021-12-31T00:00:00.000Z
                     type: object
                     properties:
                       name:
                         type: string
-                      timestamp:
-                        type: integer
-                        format: int64
-                        minimum: 0
-                        exclusiveMinimum: false
-                        maximum: 9007199254740991
-                        exclusiveMaximum: false
+                      createdAt:
+                        type: string
+                        format: date-time
+                        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
-                      - timestamp
+                      - createdAt
                 required:
                   - status
                   - data
@@ -112,7 +111,7 @@ paths:
                     status: success
                     data:
                       name: John Doe
-                      timestamp: 1235698995125
+                      createdAt: 2021-12-31T00:00:00.000Z
         "400":
           description: POST /v1/user/:id Error response
           content:
@@ -164,14 +163,23 @@ paths:
                 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
             examples:
               example1:
                 value:
                   key: 1234-5678-90
                   name: John Doe
+                  birthday: 1963-04-21
   /v1/avatar/send:
     get:
       responses:
diff --git a/package.json b/package.json
index 62eb6404..0134d8ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "express-zod-api",
-  "version": "5.4.2",
+  "version": "5.5.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 b5b106b0..411a2bea 100644
--- a/src/common-helpers.ts
+++ b/src/common-helpers.ts
@@ -161,6 +161,10 @@ export function isLoggerConfig(logger: any): logger is LoggerConfig {
   );
 }
 
+export function isValidDate(date: Date): boolean {
+  return !isNaN(date.getTime());
+}
+
 export function getMessageFromError(error: Error): string {
   if (error instanceof z.ZodError) {
     return error.issues
diff --git a/src/date-in-schema.ts b/src/date-in-schema.ts
new file mode 100644
index 00000000..ca55fdd7
--- /dev/null
+++ b/src/date-in-schema.ts
@@ -0,0 +1,63 @@
+import {
+  addIssueToContext,
+  INVALID,
+  ParseInput,
+  ParseReturnType,
+  ZodIssueCode,
+  ZodParsedType,
+  ZodType,
+  ZodTypeDef,
+} from "zod";
+import { isValidDate } from "./common-helpers";
+
+// simple regex for ISO date, supports the following formats:
+// 2021-01-01T00:00:00.000Z
+// 2021-01-01T00:00:00Z
+// 2021-01-01T00:00:00
+// 2021-01-01
+export const isoDateRegex =
+  /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?)?Z?$/;
+
+const zodDateInKind = "ZodDateIn";
+
+export interface ZodDateInDef extends ZodTypeDef {
+  typeName: typeof zodDateInKind;
+}
+
+export class ZodDateIn extends ZodType<Date, ZodDateInDef, string> {
+  _parse(input: ParseInput): ParseReturnType<Date> {
+    const { status, ctx } = this._processInputParams(input);
+    if (ctx.parsedType !== ZodParsedType.string) {
+      addIssueToContext(ctx, {
+        code: ZodIssueCode.invalid_type,
+        expected: ZodParsedType.string,
+        received: ctx.parsedType,
+      });
+      return INVALID;
+    }
+
+    if (!isoDateRegex.test(ctx.data as string)) {
+      addIssueToContext(ctx, {
+        code: ZodIssueCode.invalid_string,
+        validation: "regex",
+      });
+      status.dirty();
+    }
+
+    const date = new Date(ctx.data);
+
+    if (!isValidDate(date)) {
+      addIssueToContext(ctx, {
+        code: ZodIssueCode.invalid_date,
+      });
+      return INVALID;
+    }
+
+    return { status: status.value, value: date };
+  }
+
+  static create = () =>
+    new ZodDateIn({
+      typeName: zodDateInKind,
+    });
+}
diff --git a/src/date-out-schema.ts b/src/date-out-schema.ts
new file mode 100644
index 00000000..d36109e8
--- /dev/null
+++ b/src/date-out-schema.ts
@@ -0,0 +1,45 @@
+import {
+  addIssueToContext,
+  INVALID,
+  ParseInput,
+  ParseReturnType,
+  ZodIssueCode,
+  ZodParsedType,
+  ZodType,
+  ZodTypeDef,
+} from "zod";
+import { isValidDate } from "./common-helpers";
+
+const zodDateOutKind = "ZodDateOut";
+
+export interface ZodDateOutDef extends ZodTypeDef {
+  typeName: typeof zodDateOutKind;
+}
+
+export class ZodDateOut extends ZodType<string, ZodDateOutDef, Date> {
+  _parse(input: ParseInput): ParseReturnType<string> {
+    const { status, ctx } = this._processInputParams(input);
+    if (ctx.parsedType !== ZodParsedType.date) {
+      addIssueToContext(ctx, {
+        code: ZodIssueCode.invalid_type,
+        expected: ZodParsedType.date,
+        received: ctx.parsedType,
+      });
+      return INVALID;
+    }
+
+    if (!isValidDate(ctx.data)) {
+      addIssueToContext(ctx, {
+        code: ZodIssueCode.invalid_date,
+      });
+      return INVALID;
+    }
+
+    return { status: status.value, value: (ctx.data as Date).toISOString() };
+  }
+
+  static create = () =>
+    new ZodDateOut({
+      typeName: zodDateOutKind,
+    });
+}
diff --git a/src/extend-zod.ts b/src/extend-zod.ts
index 741e55ab..33194ce2 100644
--- a/src/extend-zod.ts
+++ b/src/extend-zod.ts
@@ -1,6 +1,18 @@
+import { ZodDate } from "zod";
+import { ZodDateIn } from "./date-in-schema";
+import { ZodDateOut } from "./date-out-schema";
 import { ZodFile } from "./file-schema";
 import { ZodUpload } from "./upload-schema";
 
 export * from "zod";
 export const file = ZodFile.create;
 export const upload = ZodUpload.create;
+
+/**
+ * @todo sort this out in the next major release
+ * @description z.date() represents the Date, it should not be used with IO schemas
+ * @deprecated Please use z.dateIn() or z.dateOut() within IO schemas
+ * */
+export const date = ZodDate.create;
+export const dateIn = ZodDateIn.create;
+export const dateOut = ZodDateOut.create;
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index df3806e7..231a485d 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -20,6 +20,8 @@ import {
   routePathParamsRegex,
 } from "./common-helpers";
 import { InputSources } from "./config-type";
+import { isoDateRegex, ZodDateIn, ZodDateInDef } from "./date-in-schema";
+import { ZodDateOut, ZodDateOutDef } from "./date-out-schema";
 import { AbstractEndpoint } from "./endpoint";
 import { OpenAPIError } from "./errors";
 import { ZodFile, ZodFileDef } from "./file-schema";
@@ -37,7 +39,11 @@ type DepictHelper<T extends z.ZodType<any>> = (params: {
 
 type DepictingRules = Partial<
   Record<
-    z.ZodFirstPartyTypeKind | ZodFileDef["typeName"] | ZodUploadDef["typeName"],
+    | z.ZodFirstPartyTypeKind
+    | ZodFileDef["typeName"]
+    | ZodUploadDef["typeName"]
+    | ZodDateInDef["typeName"]
+    | ZodDateOutDef["typeName"],
     DepictHelper<any>
   >
 >;
@@ -48,6 +54,9 @@ interface ReqResDepictHelperCommonProps {
   endpoint: AbstractEndpoint;
 }
 
+const isoDateDocumentationUrl =
+  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
+
 /* eslint-disable @typescript-eslint/no-use-before-define */
 
 export const reformatParamsInPath = (path: string) =>
@@ -166,12 +175,56 @@ export const depictNull: DepictHelper<z.ZodNull> = ({ initial }) => ({
   format: "null",
 });
 
-export const depictDate: DepictHelper<z.ZodDate> = ({ initial }) => ({
+/** @todo remove in next major update */
+export const depictDate: DepictHelper<z.ZodDate> = ({
+  initial,
+  isResponse,
+}) => ({
   ...initial,
   type: "string",
-  format: "date",
+  format: "date-time",
+  description: `z.date() is deprecated, please use ${
+    isResponse ? "z.dateOut()" : "z.dateIn()"
+  } instead.`,
 });
 
+export const depictDateIn: DepictHelper<ZodDateIn> = ({
+  initial,
+  isResponse,
+}) => {
+  if (isResponse) {
+    throw new OpenAPIError("Please use z.dateOut() for output.");
+  }
+  return {
+    ...initial,
+    type: "string",
+    format: "date-time",
+    pattern: isoDateRegex.source,
+    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
+    externalDocs: {
+      url: isoDateDocumentationUrl,
+    },
+  };
+};
+
+export const depictDateOut: DepictHelper<ZodDateOut> = ({
+  initial,
+  isResponse,
+}) => {
+  if (!isResponse) {
+    throw new OpenAPIError("Please use z.dateIn() for input.");
+  }
+  return {
+    ...initial,
+    type: "string",
+    format: "date-time",
+    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
+    externalDocs: {
+      url: isoDateDocumentationUrl,
+    },
+  };
+};
+
 export const depictBoolean: DepictHelper<z.ZodBoolean> = ({ initial }) => ({
   ...initial,
   type: "boolean",
@@ -505,7 +558,10 @@ const depictHelpers: DepictingRules = {
   ZodNumber: depictNumber,
   ZodBigInt: depictBigInt,
   ZodBoolean: depictBoolean,
+  /** @todo remove in next major update */
   ZodDate: depictDate,
+  ZodDateIn: depictDateIn,
+  ZodDateOut: depictDateOut,
   ZodNull: depictNull,
   ZodArray: depictArray,
   ZodTuple: depictTuple,


Test patch:
diff --git a/tests/system/example.spec.ts b/tests/system/example.spec.ts
index d9acd194..652676b1 100644
--- a/tests/system/example.spec.ts
+++ b/tests/system/example.spec.ts
@@ -64,6 +64,7 @@ describe("Example", () => {
         body: JSON.stringify({
           key: "123",
           name: "John Doe",
+          birthday: "1974-10-28",
         }),
       });
       expect(response.status).toBe(200);
@@ -72,7 +73,7 @@ describe("Example", () => {
         status: "success",
         data: {
           name: "John Doe",
-          timestamp: expect.any(Number),
+          createdAt: "2022-01-22T00:00:00.000Z",
         },
       });
       await waitFor(() => /v1\/user/.test(out));
@@ -203,6 +204,7 @@ describe("Example", () => {
         body: JSON.stringify({
           key: "456",
           name: "John Doe",
+          birthday: "1974-10-28",
         }),
       });
       expect(response.status).toBe(401);
@@ -225,6 +227,7 @@ describe("Example", () => {
         body: JSON.stringify({
           key: "123",
           name: "John Doe",
+          birthday: "1974-10-28",
         }),
       });
       expect(response.status).toBe(401);
@@ -247,6 +250,7 @@ describe("Example", () => {
         body: JSON.stringify({
           key: "123",
           name: "John Doe",
+          birthday: "1974-10-28",
         }),
       });
       expect(response.status).toBe(400);
@@ -264,6 +268,7 @@ describe("Example", () => {
         body: JSON.stringify({
           key: "123",
           name: "John Doe",
+          birthday: "1974-10-28",
         }),
       });
       expect(response.status).toBe(404);
diff --git a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
index 35ef3075..b6d9ad68 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -34,12 +34,39 @@ Object {
 
 exports[`Open API helpers depictDate() should depict ZodDate 1`] = `
 Object {
-  "description": "test",
-  "format": "date",
+  "description": "z.date() is deprecated, please use z.dateIn() instead.",
+  "format": "date-time",
+  "type": "string",
+}
+`;
+
+exports[`Open API helpers depictDateIn should depict ZodDateIn 1`] = `
+Object {
+  "description": "YYYY-MM-DDTHH:mm:ss.sssZ",
+  "externalDocs": Object {
+    "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString",
+  },
+  "format": "date-time",
+  "pattern": "^\\\\d{4}-\\\\d{2}-\\\\d{2}(T\\\\d{2}:\\\\d{2}:\\\\d{2}(\\\\.\\\\d{3})?)?Z?$",
   "type": "string",
 }
 `;
 
+exports[`Open API helpers depictDateIn should throw when ZodDateIn in response 1`] = `[Error: Please use z.dateOut() for output.]`;
+
+exports[`Open API helpers depictDateOut should depict ZodDateOut 1`] = `
+Object {
+  "description": "YYYY-MM-DDTHH:mm:ss.sssZ",
+  "externalDocs": Object {
+    "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString",
+  },
+  "format": "date-time",
+  "type": "string",
+}
+`;
+
+exports[`Open API helpers depictDateOut should throw when ZodDateOut in request 1`] = `[Error: Please use z.dateIn() for input.]`;
+
 exports[`Open API helpers depictDefault() should depict ZodDefault 1`] = `
 Object {
   "default": true,
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 64bab8ef..dc4e2f61 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -1455,21 +1455,20 @@ paths:
                   data:
                     example:
                       name: John Doe
-                      timestamp: 1235698995125
+                      createdAt: 2021-12-31T00:00:00.000Z
                     type: object
                     properties:
                       name:
                         type: string
-                      timestamp:
-                        type: integer
-                        format: int64
-                        minimum: 0
-                        exclusiveMinimum: false
-                        maximum: 9007199254740991
-                        exclusiveMaximum: false
+                      createdAt:
+                        type: string
+                        format: date-time
+                        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
-                      - timestamp
+                      - createdAt
                 required:
                   - status
                   - data
@@ -1479,7 +1478,7 @@ paths:
                     status: success
                     data:
                       name: John Doe
-                      timestamp: 1235698995125
+                      createdAt: 2021-12-31T00:00:00.000Z
         \\"400\\":
           description: POST /v1/user/:id Error response
           content:
@@ -1531,14 +1530,23 @@ paths:
                 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
             examples:
               example1:
                 value:
                   key: 1234-5678-90
                   name: John Doe
+                  birthday: 1963-04-21
   /v1/avatar/send:
     get:
       responses:
@@ -1712,8 +1720,15 @@ paths:
                         nullable: true
                         type: string
                         format: \\"null\\"
+                      dateOut:
+                        type: string
+                        format: date-time
+                        description: YYYY-MM-DDTHH:mm:ss.sssZ
+                        externalDocs:
+                          url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
                     required:
                       - \\"null\\"
+                      - dateOut
                 required:
                   - status
                   - data
@@ -1758,11 +1773,20 @@ paths:
                   type: boolean
                 date:
                   type: string
-                  format: date
+                  format: date-time
+                  description: z.date() is deprecated, please use z.dateIn() instead.
+                dateIn:
+                  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:
                 - bigint
                 - boolean
                 - date
+                - dateIn
 components:
   schemas: {}
   responses: {}
diff --git a/tests/unit/common-helpers.spec.ts b/tests/unit/common-helpers.spec.ts
index 3cd8e2aa..7106bdb7 100644
--- a/tests/unit/common-helpers.spec.ts
+++ b/tests/unit/common-helpers.spec.ts
@@ -12,6 +12,7 @@ import {
   getStatusCodeFromError,
   hasUpload,
   isLoggerConfig,
+  isValidDate,
   OutputMarker,
 } from "../../src/common-helpers";
 import {
@@ -734,4 +735,19 @@ describe("Common Helpers", () => {
       expect(hasUpload(z.number().or(z.string()))).toBeFalsy();
     });
   });
+
+  describe("isValidDate()", () => {
+    test("should accept valid date", () => {
+      expect(isValidDate(new Date())).toBeTruthy();
+      expect(isValidDate(new Date("2021-01-31"))).toBeTruthy();
+      expect(isValidDate(new Date("12.01.2022"))).toBeTruthy();
+      expect(isValidDate(new Date("01/22/2022"))).toBeTruthy();
+    });
+
+    test("should handle invalid date", () => {
+      expect(isValidDate(new Date("2021-01-32"))).toBeFalsy();
+      expect(isValidDate(new Date("22/01/2022"))).toBeFalsy();
+      expect(isValidDate(new Date("2021-01-31T25:00:00.000Z"))).toBeFalsy();
+    });
+  });
 });
diff --git a/tests/unit/date-in-schema.spec.ts b/tests/unit/date-in-schema.spec.ts
new file mode 100644
index 00000000..e881e26c
--- /dev/null
+++ b/tests/unit/date-in-schema.spec.ts
@@ -0,0 +1,70 @@
+import { ZodDateIn } from "../../src/date-in-schema";
+
+describe("ZodDateIn", () => {
+  describe("static::create()", () => {
+    test("should create an instance", () => {
+      const schema = ZodDateIn.create();
+      expect(schema).toBeInstanceOf(ZodDateIn);
+      expect(schema._def.typeName).toEqual("ZodDateIn");
+    });
+  });
+
+  describe("_parse()", () => {
+    test("should handle wrong parsed type", () => {
+      const schema = ZodDateIn.create();
+      const result = schema.safeParse(123);
+      expect(result.success).toBeFalsy();
+      if (!result.success) {
+        expect(result.error.issues).toEqual([
+          {
+            code: "invalid_type",
+            expected: "string",
+            message: "Expected string, received number",
+            path: [],
+            received: "number",
+          },
+        ]);
+      }
+    });
+
+    test("should accept valid date string", () => {
+      const schema = ZodDateIn.create();
+      const result = schema.safeParse("2022-12-31");
+      expect(result).toEqual({
+        success: true,
+        data: new Date("2022-12-31"),
+      });
+    });
+
+    test("should handle invalid date", () => {
+      const schema = ZodDateIn.create();
+      const result = schema.safeParse("2022-01-32");
+      expect(result.success).toBeFalsy();
+      if (!result.success) {
+        expect(result.error.issues).toEqual([
+          {
+            code: "invalid_date",
+            message: "Invalid date",
+            path: [],
+          },
+        ]);
+      }
+    });
+
+    test("should handle invalid format", () => {
+      const schema = ZodDateIn.create();
+      const result = schema.safeParse("12.01.2021");
+      expect(result.success).toBeFalsy();
+      if (!result.success) {
+        expect(result.error.issues).toEqual([
+          {
+            code: "invalid_string",
+            message: "Invalid",
+            validation: "regex",
+            path: [],
+          },
+        ]);
+      }
+    });
+  });
+});
diff --git a/tests/unit/date-out-schema.spec.ts b/tests/unit/date-out-schema.spec.ts
new file mode 100644
index 00000000..85c8e494
--- /dev/null
+++ b/tests/unit/date-out-schema.spec.ts
@@ -0,0 +1,54 @@
+import { ZodDateOut } from "../../src/date-out-schema";
+
+describe("ZodDateOut", () => {
+  describe("static::create()", () => {
+    test("should create an instance", () => {
+      const schema = ZodDateOut.create();
+      expect(schema).toBeInstanceOf(ZodDateOut);
+      expect(schema._def.typeName).toEqual("ZodDateOut");
+    });
+  });
+
+  describe("_parse()", () => {
+    test("should handle wrong parsed type", () => {
+      const schema = ZodDateOut.create();
+      const result = schema.safeParse("12.01.2022");
+      expect(result.success).toBeFalsy();
+      if (!result.success) {
+        expect(result.error.issues).toEqual([
+          {
+            code: "invalid_type",
+            expected: "date",
+            message: "Expected date, received string",
+            path: [],
+            received: "string",
+          },
+        ]);
+      }
+    });
+
+    test("should accept valid date", () => {
+      const schema = ZodDateOut.create();
+      const result = schema.safeParse(new Date("2022-12-31"));
+      expect(result).toEqual({
+        success: true,
+        data: "2022-12-31T00:00:00.000Z",
+      });
+    });
+
+    test("should handle invalid date", () => {
+      const schema = ZodDateOut.create();
+      const result = schema.safeParse(new Date("2022-01-32"));
+      expect(result.success).toBeFalsy();
+      if (!result.success) {
+        expect(result.error.issues).toEqual([
+          {
+            code: "invalid_date",
+            message: "Invalid date",
+            path: [],
+          },
+        ]);
+      }
+    });
+  });
+});
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index 0935e18b..7d7093b4 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -416,6 +416,21 @@ describe("Endpoint", () => {
         ...mInput2._input,
       });
     });
+
+    test("should handle z.dateIn() correctly", () => {
+      const factory = new EndpointsFactory(defaultResultHandler);
+      const endpoint = factory.build({
+        method: "get",
+        output: z.object({}),
+        input: z.object({
+          birthday: z.dateIn(),
+        }),
+        handler: jest.fn(),
+      });
+      expectType<EndpointInput<typeof endpoint>>({
+        birthday: "1984-12-31T00:00:00Z",
+      });
+    });
   });
 
   describe("EndpointOutput<>", () => {
@@ -457,6 +472,28 @@ describe("Endpoint", () => {
         },
       });
     });
+
+    test("should handle z.dateOut() correctly", () => {
+      const factory = new EndpointsFactory(defaultResultHandler);
+      const endpoint = factory.build({
+        method: "get",
+        input: z.object({}),
+        output: z.object({
+          birthday: z.dateOut(),
+        }),
+        handler: jest.fn(),
+      });
+      expectType<EndpointResponse<typeof endpoint>>({
+        status: "success",
+        data: { birthday: "1984-12-31T00:00:00Z" },
+      });
+      expectType<EndpointResponse<typeof endpoint>>({
+        status: "error",
+        error: {
+          message: "some error",
+        },
+      });
+    });
   });
 
   describe("Issue #269: Async refinements", () => {
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 39a57276..5522fed7 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -1,10 +1,17 @@
-import { defaultEndpointsFactory, withMeta, z } from "../../src/index";
+import {
+  defaultEndpointsFactory,
+  OpenAPIError,
+  withMeta,
+  z,
+} from "../../src/index";
 import {
   depictAny,
   depictArray,
   depictBigInt,
   depictBoolean,
   depictDate,
+  depictDateIn,
+  depictDateOut,
   depictDefault,
   depictEffect,
   depictEnum,
@@ -638,4 +645,54 @@ describe("Open API helpers", () => {
       ).toMatchSnapshot();
     });
   });
+
+  describe("depictDateIn", () => {
+    test("should depict ZodDateIn", () => {
+      expect(
+        depictDateIn({
+          schema: z.dateIn(),
+          isResponse: false,
+          initial: { description: "test" },
+        })
+      ).toMatchSnapshot();
+    });
+    test("should throw when ZodDateIn in response", () => {
+      try {
+        depictDateIn({
+          schema: z.dateIn(),
+          isResponse: true,
+          initial: { description: "test" },
+        });
+        fail("should not be here");
+      } catch (e) {
+        expect(e).toBeInstanceOf(OpenAPIError);
+        expect(e).toMatchSnapshot();
+      }
+    });
+  });
+
+  describe("depictDateOut", () => {
+    test("should depict ZodDateOut", () => {
+      expect(
+        depictDateOut({
+          schema: z.dateOut(),
+          isResponse: true,
+          initial: { description: "test" },
+        })
+      ).toMatchSnapshot();
+    });
+    test("should throw when ZodDateOut in request", () => {
+      try {
+        depictDateOut({
+          schema: z.dateOut(),
+          isResponse: false,
+          initial: { description: "test" },
+        });
+        fail("should not be here");
+      } catch (e) {
+        expect(e).toBeInstanceOf(OpenAPIError);
+        expect(e).toMatchSnapshot();
+      }
+    });
+  });
 });
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index 75dd4257..d0b6ab5c 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -206,12 +206,15 @@ describe("Open API generator", () => {
                 bigint: z.bigint(),
                 boolean: z.boolean(),
                 date: z.date(),
+                dateIn: z.dateIn(),
               }),
               output: z.object({
                 null: z.null(),
+                dateOut: z.dateOut(),
               }),
               handler: async () => ({
                 null: null,
+                dateOut: new Date("2021-12-31"),
               }),
             }),
           },